diff --git a/docs/memory/PROJECT_MEMORY.md b/docs/memory/PROJECT_MEMORY.md index 46b8e2c..f98e778 100644 --- a/docs/memory/PROJECT_MEMORY.md +++ b/docs/memory/PROJECT_MEMORY.md @@ -197,7 +197,8 @@ - Rule: A static comparison of the assertion string against the target file only proves the string exists somewhere in the file, not that the assertion targets the right file or would actually catch a regression. Spot-check the highest-risk new pins with a mutate-and-revert: temporarily alter the pinned phrase in the source file, run the specific pin test and confirm it goes red with the expected diagnostic, then revert and confirm `git status` is clean again. Do this for a handful of the riskiest new pins (cross-file pointers, security guards), not every single one. A read-only agent that cannot mutate the file (e.g. `argos` in CR) substitutes a static equivalent: grep the pinned string's exact occurrence count across the target file — a pin anchored on a short phrase that also recurs elsewhere (a boilerplate heading, a repeated label) is provably not load-bearing regardless of execution, while a pin whose exact string has exactly one occurrence is a much stronger proxy. - Example: PR #749 (issue #748) — apollon ran 3 mutate-and-revert spot checks (a rule-guard phrase, a skill cross-pointer, an agent `tools:` entry) and confirmed each corresponding pin test failed red with the exact expected diagnostic before reverting, ruling out false-green pins. - Example: PR #775 (issue #774), round 1 — argos (read-only) flagged the pin `**Gating — raise one finding per violation, never both:**` in `tests/Installer/CodeReviewContentTest.php` as not load-bearing because that exact phrase already had 4 other occurrences in the same file, so deleting the new sentence it claimed to protect would leave the test green. Round 2 — talos re-anchored the pin on the full unique sentence and independently confirmed all 20 new pinned strings had exactly 1 occurrence each before re-review; argos then closed the finding by recomputing the same occurrence counts rather than trusting the implementer's report. -- Source: https://github.com/pekral/cursor-rules/pull/749 Added: 2026-07-31 Updated: 2026-08-18 (PR #775) +- Example: PR #782 — the mutation was reverted with `git checkout -- rules/compound-engineering/general.mdc`, which restores the file from the **index**, so it deleted the entire uncommitted section the pin was protecting, not just the mutation. `git status` then looked clean, which is exactly what makes the loss easy to miss. Undo a spot-check mutation with the **inverse `sed`** (or stash/restore) whenever the file carries uncommitted work; `git checkout --` is only safe once the work is committed. +- Source: https://github.com/pekral/cursor-rules/pull/749 Added: 2026-07-31 Updated: 2026-08-25 (PR #782) - Role: shared ### verify-a-referenced-skill-exists-before-copying-a-lens-list — A CR lens list can name a skill the package never shipped; verify against `ls skills/` before propagating it @@ -256,3 +257,11 @@ - Example: PR #775 (issue #774) — `rules/php/examples/self-documenting-code.md`'s first pair (`passport` guard) correctly kept `@see ECOMAIL-6655` after renaming, but the second pair (`MAX_WEBHOOK_DELIVERY_ATTEMPTS`) dropped the "Stripe recommends this" *why* residue entirely and the third pair's rename lost the "before matching it against existing customers" purpose. Both were CR Moderate findings in round 1; fixed by adding `@see https://docs.stripe.com/webhooks#retry-logic` and renaming to `normalizePhoneNumberForCustomerMatch`. - Source: https://github.com/pekral/cursor-rules/pull/775 Added: 2026-08-18 - Role: implementation + +### pest-tocontain-is-variadic-not-needle-plus-message — `toContain($needle, $message)` searches for the message as a second needle + +- Trigger: adding a pinning assertion in `tests/Installer/*ContentTest.php` inside a `foreach` over several skills / files, where a per-iteration failure message would say which one failed. +- Rule: Pest's string `toContain()` takes **variadic needles**, not `(needle, message)` — a message passed as the second argument becomes a second string the file must contain, so the assertion fails on text that was never meant to be in the file, and the diagnostic points at the message instead of the real gap. `toBe()` *does* take a message. When a loop needs both a per-item message and the assertion, use `expect(substr_count($haystack, $needle))->toBe(1, $message)`: it carries the message, and it is strictly stronger than `toContain()` because it also pins uniqueness, which the repo's own [[verify-pin-tests-load-bearing-via-mutate-and-revert]] lesson asks for anyway. +- Example: PR #782 — two new pin tests looped over six wired skills with `expect($skill)->toContain($needle, $slug . ' cites the canonical section')`; both went red against files that did contain the needle. Rewriting them as `substr_count(...)->toBe(1, $slug . ' …')` fixed the diagnostic and upgraded the pins to uniqueness checks in the same edit. +- Source: https://github.com/pekral/cursor-rules/pull/782 Added: 2026-08-25 +- Role: implementation