Document the plugin, and check that the documentation still describes it

Five files under docs/, the names the family agreed: README, SETUP,
ARCHITECTURE, CONTRACT, RELEASE. Plus ci/check-docs.php, ported from
woo-security-manager 52dd99d, wired into the static job.

CONTRACT.md is generated from the source rather than written from memory, and
that was the right instruction: my first sweep read only single-quoted hook
names and missed two rows, one of them update_plugins_{$hostname} — the hook
behind the live-shop collision, written with interpolation and therefore
invisible to a pattern built from the concept of the line. Nine hooks
documented: six wc_edm_* filters this plugin offers, three wpmgr_* it both
offers and answers through its copy of the frozen shared-menu.php.

Two hooks the checker structurally cannot see are listed by hand with a note
saying so: wc_edm_prune_audit_log (add_action on a class constant) and
update_plugins_{$hostname} (interpolated). A generated list beats a remembered
one and is still a list of what the generator can see.

The checker needed two fixes before it was safe to run here.

🔴 The published version reads HOOK_PREFIX from ci/plugin.conf for the
documented side, and refuses without it — then tests the source side against a
hardcoded 'nlsec_'. Ported verbatim that is not a green over nothing, which
would be easy to spot. Measured in this tree with CONTRACT.md documenting no
hook at all:

  3 hook(s) in the shipped source and not in docs/CONTRACT.md:
  wpmgr_dashboard_cards, wpmgr_menu_glyph, wpmgr_menu_title

Red, specific, and blind to all six wc_edm_* hooks — because 'wpmgr_' is the
same string in every tree and 'nlsec_' is the one that is not. Fix those three
and it goes green over six undocumented contract hooks, with a red run behind
you as evidence that it works. A partially blind check that fails is more
convincing than one that passes.

Note which step disarmed the guard. The floor at 20 shipped PHP files refused
to run in this 14-file tree, and that refusal was the only thing standing in
the way; the porting instructions were to add HOOK_PREFIX and adjust the floor.
Doing as told is what removed the protection — two wrongs cancelling into a
pass, in the order CLAUDE.md predicts, because the floor is the error you can
see and the prefix is not.

Both numbers now come from the conf (HOOK_PREFIX, MIN_SHIPPED_PHP_FILES) and
both refuse rather than defaulting. The inherited literal 20 is gone: a second
hardcoded number per plugin is a second thing to re-measure on every port.

Ten selftest assertions, 58 total. Four failure modes, the two refusals, the
counts on the pass, and the row that matters most:

🔴 A hook name in a COMMENT must not read as a hook call. CONTRACT.md documents
a filter by showing the apply_filters() a consumer makes, and the same example
sits in a docblock — so a checker that reads comments reports this plugin
consuming a filter it answers, in the file explaining why it must not. That
assertion is a control asserting a PASS, which is the kind that can quietly
become unable to fail, so it names the specific success line rather than
reading an exit code, and it is proven both ways: with token_get_all's
T_COMMENT branch made inert, exactly one of 58 assertions fails and it is that
one.

docs/TESTING-AND-RELEASES.md is folded into RELEASE.md and deleted rather than
left beside it — two copies of a rule is how the copies start to disagree.
Folding surfaced that it still documented the pre-rename Update URI
(kw/woocommerce-email-discount-manager) and still recommended, as future work,
the split into a releases-only repository that has already happened. Both
corrected in place, with the correction dated.

docs/ is not in ci/dist-files.sh — measured, 0 docs files in the zip.

static=0 selftest=0 site=0; site still 69 assertions on WordPress 7.1.2 with
WooCommerce 11.1.2. ci/ and docs/ ship nothing, so no version bump.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kristianwind
2026-09-28 21:45:29 +02:00
co-authored by Claude Opus 5
parent ffa7f5da3a
commit dccb87d15e
10 changed files with 1255 additions and 263 deletions
+104
View File
@@ -370,6 +370,110 @@ else
bad "only $pairs pairs compared — the agreement check has almost nothing to look at"
fi
# ------------------------------------------------------------------------ docs
# 🔴 ci/check-docs.php has four failure modes and one subtle exclusion, and the
# exclusion is the one that would regress unnoticed: it strips comments before
# reading the source. Without that, a CONTRACT.md example shown in a docblock
# reads as a real hook call — so a file documenting a contract is reported as
# breaking it. Nolimit Security proved all four by hand in one sitting; four
# commands in a commit message are not an assertion that runs again next month.
#
# Unlike the git-identity rows, these fixtures cannot trip the checker over
# themselves: it reads only what ci/dist-files.sh ships, and ci/ and tests/ are
# not in that list. Verified rather than assumed — dist-files.sh returns no
# ci/ path at all.
echo
echo "→ ci/check-docs.php must catch a document that stopped describing this tree"
cd "$work/repo"
docs_verdict() { ( . ci/lib.sh && ci_load_conf && ci_php ci/check-docs.php . ) 2>&1 || true; }
# 1. a hook in the source that no document mentions.
cp includes/class-helpers.php "$work/helpers.orig"
printf '\n// Added by ci/selftest.sh.\nfunction wc_edm_selftest_probe() {\n\treturn apply_filters( %s, true );\n}\n' \
"'wc_edm_selftest_undocumented'" >> includes/class-helpers.php
if [ "$(grep -c 'wc_edm_selftest_undocumented' includes/class-helpers.php)" -lt 1 ]; then
bad "could not stage the undocumented-hook fixture"
else
printf '%s' "$(docs_verdict)" | grep -q 'not in docs/CONTRACT.md: wc_edm_selftest_undocumented' \
&& ok "flagged a hook the source offers and CONTRACT.md does not mention" \
|| bad "NOT FLAGGED: an undocumented hook — a sibling cannot discover it without reading our source"
fi
cp "$work/helpers.orig" includes/class-helpers.php
# 2. 🔴 the control that matters: the same name, in a COMMENT only. The checker
# must stay GREEN, and this asserts the specific success line rather than an
# exit code — a refusal, a missing file or a fatal all exit non-zero too, and
# any of them would read as "the comment was ignored".
printf '\n// apply_filters( %s, true ) — an example in prose, not a call.\n' \
"'wc_edm_selftest_in_a_comment'" >> includes/class-helpers.php
if [ "$(grep -c 'wc_edm_selftest_in_a_comment' includes/class-helpers.php)" -lt 1 ]; then
bad "could not stage the comment-only fixture"
else
comment_out=$(docs_verdict)
if printf '%s' "$comment_out" | grep -qE 'all [0-9]+ in docs/CONTRACT.md'; then
ok "a hook name in a comment is not a hook call (comment stripping still works)"
else
printf '%s\n' "$comment_out" | sed 's/^/ /'
bad "a hook name in a COMMENT was read as a call — a docblock documenting a contract now breaks the build"
fi
fi
cp "$work/helpers.orig" includes/class-helpers.php
# 3. the other direction: a row for a hook that no longer exists. Silent without
# this check, and the row reads as deliberate.
cp docs/CONTRACT.md "$work/contract.orig"
printf '\n| `wc_edm_selftest_renamed_away` | `bool $x` | `true` | nothing |\n' >> docs/CONTRACT.md
if [ "$(grep -c 'wc_edm_selftest_renamed_away' docs/CONTRACT.md)" -lt 1 ]; then
bad "could not stage the stale-row fixture"
else
printf '%s' "$(docs_verdict)" | grep -q 'wc_edm_selftest_renamed_away' \
&& ok "flagged a CONTRACT.md row whose hook is not in the source" \
|| bad "NOT FLAGGED: a stale CONTRACT.md row — the document describes a tree that is not this one"
fi
cp "$work/contract.orig" docs/CONTRACT.md
# 4. a document emptied to its heading. "Exists" passes over a stub.
cp docs/SETUP.md "$work/setup.orig"
printf '# Setting it up\n' > docs/SETUP.md
printf '%s' "$(docs_verdict)" | grep -q 'docs/SETUP.md has 1 lines' \
&& ok "flagged a document emptied to its heading" \
|| bad "NOT FLAGGED: a one-line SETUP.md — 'the file exists' is not 'the file says anything'"
cp "$work/setup.orig" docs/SETUP.md
# 5. the version drifting away from the header.
sed -i 's/^Version 1\.[0-9]*\.[0-9]*/Version 9.9.9/' docs/README.md
if [ "$(grep -c '^Version 9.9.9' docs/README.md)" -lt 1 ]; then
bad "could not stage the version-drift fixture"
else
printf '%s' "$(docs_verdict)" | grep -q 'does not mention version' \
&& ok "flagged docs naming a version the header does not" \
|| bad "NOT FLAGGED: docs at 9.9.9 against a different header — the docs describe another release"
fi
git checkout -q -- docs/README.md 2>/dev/null || cp "$work/README.md.orig" docs/README.md
# 6. both conf values must refuse rather than default. An unset floor parses to
# 0 and every comparison against it passes, and a guessed prefix classifies
# every hook in the tree as somebody else's.
cp ci/plugin.conf "$work/conf.orig"
for key in HOOK_PREFIX MIN_SHIPPED_PHP_FILES; do
grep -v "^$key=" "$work/conf.orig" > ci/plugin.conf
if [ "$(grep -c "^$key=" ci/plugin.conf)" -ne 0 ]; then
bad "could not stage the missing-$key fixture"
continue
fi
printf '%s' "$(docs_verdict)" | grep -q "No .*$key" \
&& ok "refuses to run with no $key rather than guessing" \
|| bad "ran without $key — a guessed prefix or a zero floor passes over everything"
done
cp "$work/conf.orig" ci/plugin.conf
# And the pass must carry both numbers. "The documentation matches" over zero
# hooks in zero files is the same green as over nine in fourteen.
docs_clean=$(docs_verdict)
printf '%s' "$docs_clean" | grep -qE '^ [0-9]+ hooks in [0-9]+ shipped PHP files, all [0-9]+ in docs/CONTRACT.md' \
&& ok "the docs check reports how many hooks and how many files it read" \
|| bad "the docs check printed no counts — it cannot tell 9 hooks in 14 files from 0 in 0"
# ------------------------------------------------------------------- plugin.conf
echo
echo "→ the floors must be required, not defaulted"