Releases: SNO7E-G/Sediment
Release list
v0.3.0
Completes the grading rubric and the artifact surface. Grade B was published
from the start but could never be assigned, because nothing detected the
conditional cleanup it describes; that gap is now closed, and the last artifact
types the manifest reserved are detected.
Added
-
Conditional cleanup detection, and with it grade B. An uninstall routine
gated on a stored setting —if (!get_option('..._delete_data')) return;, or
the same removals wrapped inside theif— is now recognised, and the gating
option and its default are reported in the cleanup block and the manifest.
This is the plugin that is technically clean and practically dirty: the setting
almost always defaults to off, so on a real site nothing is removed. Naming it
B is more useful than folding it into A or C.A guard only counts when it actually gates cleanup — it bails out early, or a
removal sits inside it — so an unrelatedif (get_option('schema') === 'v2')
in the same routine does not cost an otherwise clean plugin its A. -
Directories —
wp_mkdir_pandmkdir, withWP_CONTENT_DIR,
WP_PLUGIN_DIR, andABSPATHrewritten to portable{content_dir},
{plugin_dir}, and{abspath}tokens, the same way{prefix}works for
tables. A path that is only a root with nothing under it is skipped rather than
reported as a directory the plugin created. -
Rewrite rules —
add_rewrite_rule,add_rewrite_endpoint, and
add_rewrite_tag. They weigh lightly: a rule is one entry in a single option
and disappears on the next permalink flush. -
Action Scheduler jobs —
as_schedule_recurring_action,
as_schedule_single_action,as_schedule_cron_action, and
as_enqueue_async_action. A queued job behaves like a cron event, so it weighs
like one.
Fixed
-
A cron event scheduled with arguments is no longer reported as cleaned by an
argument-lesswp_clear_scheduled_hook(). That call only removes events
registered without arguments, so the event actually survives and keeps firing —
Sediment was crediting a cleanup that does not happen. Clearing it needs
wp_unschedule_hook(), which is what the generateduninstall.phpnow emits
for those events. -
The
$wpdbhandle is recognised when held as a property ($this->wpdb),
not only as the global variable, so table creation and drops written that way
are no longer missed. Recognition lives in one helper rather than three
hand-rolled checks, and still requires the namewpdb— an alias under a
different name is missed rather than guessed at. -
Cleanup detection for the new types —
as_unschedule_all_actions,
as_unschedule_action,rmdir, andflush_rewrite_rules— so a plugin that
tidies up after itself is credited for it.
Changed
- A key written from several places counts as cleaned only when every write of
it is cleaned. A hook scheduled both with and without arguments is one key
with two fates; the previous "any write cleaned" merge could report such a
plugin as spotless while an event kept firing. - Scores are held inside the band their letter allows, so a C with one stray
transient can no longer outrank a B on a leaderboard. - Grade summaries no longer state which way a gating setting must be set.
Sediment sees which option decides, not the polarity of the comparison, and
"keep my data" gates are as common as "delete my data" ones — so the wording
names the setting and its default rather than asserting a direction. - Every artifact type the manifest reserves is now populated by detection; the
createsgroups are unchanged, so consumers need no update. A finding type
with no group now raises an error instead of vanishing from the document. - WordPress's own directories (
uploads,plugins,themes, …) are recognised
as core, so they are never attributed to a plugin or offered for deletion.
Security
- A table name cut short by a dynamic tail is no longer claimed. Resolving
"CREATE TABLE {$wpdb->prefix}logs{$suffix}"stops mid-name, and treating the
fragment as the table invented one the plugin never creates — which a generated
uninstall.phpwould then drop. Such names now require proof that the name
ended before the cut. wp_clear_scheduled_hook($hook, $args)no longer credits an argument-less
event, since it clears only events registered with those exact arguments.- The
$wpdbhandle is accepted only as$wpdb,$this->wpdb, or
self::$wpdb, so an unrelated object's->wpdbcannot credit a table drop.
v0.2.0
Coverage expansion and the machine-readable output. The analyzer now sees the
artifact types that prefix-matching tools structurally cannot — metadata, roles,
and custom content types — and can be consumed by other tools and by CI.
Added
- Metadata detection —
add_*_meta/update_*_metafor posts, users, terms,
and comments, plusregister_meta. Becauseregister_meta's object type comes
from its first argument rather than the function name, that argument is resolved
and mapped; if it does not resolve to one of the four known literals, nothing is
emitted rather than guessing which meta table is touched. - Roles and capabilities —
add_role(including the capability names in its
literal capabilities array) and$role->add_cap(). - Custom content types —
register_post_typeandregister_taxonomy. This is
the class of leftovers competitors miss entirely: uninstall an e-commerce plugin
and its products remain as unreachable rows inwp_posts, often tens of
thousands of them. Prefix matching cannot see this; source parsing can. sediment scan --jsonemits the manifest (schema1.0): plugin metadata
read from the plugin header, grade and score, coverage counts with a resolution
rate, the cleanup path, and every artifact grouped by key with its confidence,
cleanedflag, and all thesourcesthat write it. Unresolvable writes are
listed underunresolvedrather than hidden, and artifact types not yet
detected ship as empty arrays so the schema never breaks for consumers. This is
the contract every downstream consumer reads — CI, the Index, and the WordPress
plugin — instead of reaching into the analyzer.sediment check --fail-on=<grade>exits non-zero when a plugin grades worse
than a threshold, so a plugin author can gate their own CI on their database
footprint the same way they gate on tests.docs/manifest-schema.mddocuments the manifest and the guarantees a consumer
can rely on — every group always present,{prefix}never expanded,sources
always an array,cleanedper item, andunresolvedreported rather than
hidden.- Releases are now published straight from this changelog: when the top section
carries a date instead of "unreleased", pushing it tomaintags the commit and
publishes the release with those notes. - Cleanup detection for the new types:
delete_post_meta,
delete_post_meta_by_key,delete_user_meta,delete_term_meta,
delete_comment_meta,delete_metadata(with its object type resolved), and
remove_role.
Changed
- A post type left behind now caps the grade at D. Orphaned content is rows in
wp_poststhat no longer render anywhere, which is as damaging as an orphaned
table. The rubric indocs/grading.mdand the README was updated to match. - The grader weighs the new artifact types by damage: a post type like a table,
metadata above a plain option because it multiplies per object, roles and
capabilities because every user carries them. - The generated
uninstall.phpremoves metadata (delete_post_meta_by_key/
delete_metadata) and roles (remove_role), and deliberately never deletes
posts or terms — an uninstall routine must not destroy user content silently.
Registered post types and taxonomies are listed as a comment for a human to
decide on.
v0.1.1 — Correctness & safety hardening
Sediment v0.1.1 — Correctness & safety hardening
A focused pass after a full adversarial review of the analyzer. There are no new
commands; the analyzer is simply more conservative and more correct. Every change
errs toward under-claiming rather than guessing — a key is reported as owned,
cleaned, or safe to delete only when the source genuinely proves it.
Fixed
- Cross-namespace resolution. Names resolve to their fully-qualified form, so
two classes or functions that share a short name in different namespaces no
longer cross-resolve, and a same-named method in an unrelated class can no
longer credit an uninstall callback. - Local-variable binding. Variables bound by function parameters,
foreach,
global,static,catch, and destructuring now resolve todynamic; a
single literal assignment can no longer claim a variable whose value varies at
runtime and flow into a generated deletion. - Anonymous classes are keyed per file, closing a cross-file symbol leak.
- Table SQL is read with one anchored, statement-aware parser shared by
detection and cleanup:CREATE TABLEinside an INSERT value is ignored, and
every statement in a multi-statementdbDeltais captured. - Cleanup scoping. Only confident removals count, and only inside the
plugin-rootuninstall.php(a top-level statement or a function it invokes) or
a registered callback matched case-insensitively. A dead function in
uninstall.php, a$wpdb->prepare(...)-only drop, and apattern/dynamic
removal no longer credit cleanup. - Never crash. A scan survives an unreadable directory, and the first pass is
guarded like the second.
Added
- The WordPress core allowlist now covers roles (
wp_user_rolesand friends),
uninstall_plugins,recently_activated, and more core cron hooks — so a
generateduninstall.phpcan never emit a delete for them.
Changed
- The generator rebuilds the
{prefix}token from$wpdb->prefixfor options,
cron, and transients (not only tables), wherever it appears in a key. - The grader counts unique keys rather than call sites, treats unknown-autoload
options as autoloaded, caps the score when there is no uninstall routine, and
reports low coverage instead of "creates no data" for an all-dynamic plugin. - Scanned source is escaped before it reaches the terminal report.
Full details are in the changelog.
v0.1.0 — Analyzer preview
Sediment v0.1.0 — Analyzer preview
The first public preview: a complete static analyzer for a WordPress plugin's
database footprint, from detection through grading and teardown generation. It
reads source only — no WordPress runtime, no database — and runs on PHP 8.3+.
What it does
- Detection. Options (with the autoload flag), custom tables (
dbDeltaand
CREATE TABLEvia$wpdb->query, with$wpdb->prefixresolved to a
{prefix}token), cron events (with recurrence), and transients. - Static resolution. Keys built from
define()/const, class constants
(self::,Foo::),$this->properties, string concatenation, string
interpolation, and local variables — each carrying an honest confidence level
(verified,resolved,pattern,dynamic). It never presents a guess as a
fact, and reports the fraction of write calls it could resolve. - Cleanup diff. Parses
uninstall.phpandregister_uninstall_hook
callbacks with the same engine and marks every artifact cleaned or left behind. - Grading.
sediment gradereturns an A–F letter and a 0–100 score weighted
by real-world damage (an orphaned autoloaded option outweighs many small rows). - Generation.
sediment uninstallwrites aphp -l-valid teardown that
removes only the high-confidence, non-core artifacts a plugin leaves behind. - Safety. A WordPress core allowlist and a dedicated
core-protectionCI job
assert that core artifacts can never enter a deletable set.
Install
composer require --dev sediment/analyzer
# or from a clone
git clone https://github.com/SNO7E-G/Sediment.git && cd Sediment && composer install
php bin/sediment scan path/to/plugin
php bin/sediment grade path/to/plugin
php bin/sediment uninstall path/to/plugin > uninstall.phpFull details are in the changelog.