Sync master from upstream - #120
Merged
Merged
Conversation
…ycle Addresses the WPML compatibility team's review of the 2.11.0 integration: 1. Backfill existing forms — new String_Backfill runs String_Collector::collect() once per plugin version (Action Scheduler, one job per form) when a provider becomes active, so forms created before the integration register their String Packages without a manual re-save. 2. Form title is now registered + translated. Previously post_title rendered as the form heading / instant-form banner but was never a translatable string. 3. Associate each String Package with its form post (post_id in the descriptor) so WPML 'Translate Everything Automatically' queues it with the post. 4. Delete the String Package when a form is permanently deleted — new Provider::delete_package() + before_delete_post handler, so orphaned packages and translations don't linger. Adds unit coverage for the backfill guard/idempotency, title registration + translation, package deletion, and the post_id association.
- Add the per-function tests the coverage check requires: delete_package() for the Provider interface, Null and WPML providers, and title_name() for String_Translator. - WPML_Provider::delete_package() uses ?? instead of isset()-ternary (PHP Insights style).
…, test hardening
- Backfill done-marker keyed to a dedicated SCHEMA_VERSION constant instead of
SRFM_VER, so ordinary releases no longer re-enqueue a job per form on every
update; marker stored with autoload=false (admin-only).
- Enqueue with the Action Scheduler unique flag so concurrent/interrupted
passes dedupe by (hook, args, group) instead of double-queuing.
- Backfill query includes the 'future' post status; drop redundant int casts.
- on_form_delete() builds a lightweight { name, kind } descriptor instead of
the full form_package() (no get_the_title/get_edit_post_link on delete).
- form_package() docblock lists post_id; note cms_id as the WPML fallback
pending live verification (tracked in #2942).
- Tests: assert one Action Scheduler job per form (via as_has_scheduled_action,
shim fallback); backfill_one() actually registers the form's strings;
permanent-delete-vs-trash through the real before_delete_post hook;
provider-inactive delete bail; empty-title skip; WPML delete_package
missing-kind branch. Backfill marker tests assert SCHEMA_VERSION.
WPML's package-level Translate Everything Automatically (TEA) gate reads the wpml_active_string_package_kinds filter to decide which string-package kinds to auto-translate. We registered one package per form (kind 'SureForms Form') and associated it via post_id, but never declared the kind on that filter, so TEA skipped SureForms form packages. Declare it from String_Collector, keyed by sanitize_title( PACKAGE_KIND ) = 'sureforms-form' — the same slug WPML derives for our packages, so the declaration matches already-registered packages. post_id is retained. Verified: filter output shape + registration (live wp eval), PHPCS, PHPStan L9 clean, plus two unit tests.
Master to Dev 2.12.2
Dev to Next Release 2.12.2
The admin Entries search ("Search entries...") only built two WHERE
conditions: an exact ID match (numeric terms only) and form_id IN (...)
resolved from form titles. form_data was never searched, and a non-numeric
term matching no form title forced an always-empty ID = 0 condition - so
text a respondent submitted could never be found. Both the React admin REST
endpoint (/sureforms/v1/entries/list) and the Abilities/MCP list-entries
funnel into the same Entries::get_entries(), so both missed.
Fix: add a form_data LIKE condition to the search OR-group for every
non-empty term (numeric too - phone numbers, zip codes), and drop the dead
forced-empty fallback. form_data stores plain JSON (encode_json uses
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), so LIKE matches submitted
values textually including emails, URLs and non-ASCII input. The Base query
compiler already supports LIKE and wraps the value in "%...%" itself;
esc_like() neutralizes user-typed wildcard characters. Also corrects the
list-entries ability description ("by entry ID" -> ID, form title, or
submitted form data).
Verified end-to-end on a live install: searching a submitted name returns
its entry; a gibberish term returns zero; numeric ID and form-title search
behavior unchanged. Adds unit tests for the new condition, the removed
forced-empty fallback, and wildcard escaping.
The form_data LIKE cannot use an index, so guard it for low-resource hosts: - Backend (authoritative, covers REST + MCP ability): skip the form_data LIKE for text terms under 3 characters (mb_strlen, so multibyte input counts correctly). Numeric terms are exempt - they resolve to an exact, indexed entry-ID lookup. A short text term matching no form title forces an empty result again (an OR-group with no conditions would be dropped by the query compiler and return every entry). - Admin UI: Enter-to-search ignores non-numeric terms under 3 characters; empty submit still clears the search. - Ability description documents the minimum. Tests: short text term produces no form_data condition and forces an empty result; short numeric term still matches the entry ID without scanning. Verified live: 3+ char text matches, 2-char text returns 0, numeric ID exact match works at any length.
With a search term active, the pagination COUNT includes the unindexed form_data LIKE - a full scan of candidate rows re-executed on every pagination click for an answer that cannot change between clicks. Cache it for 30 seconds keyed on the exact WHERE conditions, halving the cost of every search request and making pagination clicks scan-once. Unsearched listings stay uncached so totals reflect trash/delete mutations immediately. Verified live: page 2 of a search serves the total from the transient; unsearched listings compute fresh.
normalizeCSSVariablesForDarkBackground() appended the dark-dropdown CSS
variable to the form container's inline <style> via an unguarded
querySelector('style').innerHTML. Forms rendered with default styling
disabled (#2925) omit that inline <style> block entirely, and with the
derived variables gone the --srfm-color-input-text luminance check
degenerates so the inject branch runs every time - an
"Uncaught TypeError: Cannot read properties of null (reading 'innerHTML')"
on every de-skinned form, which also aborts the forEach over form
containers and breaks initialization of every later form on the page.
Fix:
- Skip the variable adjustment for containers carrying the
srfm-styling-none marker class - the default stylesheet that consumes
--srfm-expandable-menu-background is not enqueued for them, so the
injection was pointless as well as crashy. The srfm-has-dark-bg class
hook still applies (works without the default stylesheet; documented
for custom styling).
- Create the <style> tag when missing instead of dereferencing null, so
no other markup shape (e.g. custom-CSS-only variant) can resurface the
TypeError.
Front-loads Contact Form Builder in the title while retaining AI Forms, Payment Form, Survey and Quiz. Separates SureForms Free vs Business form types, retains expanded feature explanations, all 16 blocks, FAQs, compatibility, third-party disclosures, changelog and Upgrade Notice. Only readme.txt changed. Recreates PR #2964 on a clean branch off the latest master.
The repo lagged at 7.0.1 while the live wp.org page already shows 7.0.2 (bumped directly on wp.org, never mirrored to git). Aligning here so this readme deploy does not overwrite/regress the live compatibility value.
Mirrors the AI-first readme.txt rewrite and Tested up to 7.0.2 into the GitHub-facing README.md via the canonical wp_readme_to_markdown task.
…efresh SureForms - Contact Form Builder, AI Forms, Payment Form, Survey & Quiz
…write)
The `language` column added in 2.11.0 shipped a combined migration:
ALTER TABLE wp_srfm_entries ADD COLUMN language VARCHAR(20),
ADD INDEX idx_form_id_language (form_id, language)
On some database engines the ADD INDEX cannot see the column being added in
the same statement, so the ALTER fails ("column does not exist: language")
and re-runs on every init/plugins_loaded, flooding the error log. Where the
column ends up absent, the submission INSERT (which supplied a `language`
value) fails too and the form returns "Unable to submit form. Please try
again."
Remove the column from the submission path entirely:
- entries table: drop `language` from the schema, the CREATE definition, the
`idx_form_id_language` index, the new-columns migration, and the orderby
allowlist. Existing installs keep the (now-unused) column and its data; the
failing ALTER simply stops running.
- form-submit: stop writing `language` in the entry data. The local language
detection is kept only where it renders the confirmation/email in the
visitor's language at submit time — that never needed the stored column.
- list-entries ability: drop `language` from the orderby enum, the output
schema, and the per-entry output mapping.
- tests: assert the column/index are absent from the migration and the ability
no longer exposes `language`.
Note: the separate `wp_srfm_draft_submissions` migration errors in the same
log (session_id / drop-primary-key) originate in SureForms Pro and are out of
scope here.
The Payment History stylesheet (srfm-payment-history) was enqueued on every front-end page regardless of whether the block/shortcode was present. Gate the CSS (and JS) on actual presence so it no longer loads site-wide, while keeping the render-time enqueue so page-builder / FSE / widget placements still load it. Includes the review follow-ups from #2971: - Withhold the script + localized nonce from logged-out visitors (CSS still loads so the login message stays styled); the cancel handler re-checks auth server-side and has no wp_ajax_nopriv_ endpoint. - Cover the has_block() gate with a hook-time test using real block markup. - Make the test helper exception-safe (try/finally around the filter stack). - Add a hook-time no-WP_Post early-return test + logged-in/out JS-gate coverage. Recreated on a clean branch off the latest dev (the previous branch could not be synced with dev because dev carries a pre-ruleset unsigned commit that the "require verified signatures" rule blocks from propagating).
Adi's review: the backend dropped the entries `language` column but the React admin still rendered a sortable Language column that would always show "-" and send a now-rejected orderby=language on header click. - EntriesTable.js: remove the Language column definition. - useEntriesSort.js: drop `language` from columnToApiFieldMap so its header can no longer emit orderby=language. - entryHelpers.js: stop mapping entry.language in transformEntry. - form-submit.php: refresh the now-stale comments — $entry_language only drives the submit-time switch_language() for confirmation/email rendering; it is no longer persisted to any column. Pro read-paths checked (finding #2): no SureForms Pro code reads or orders by the entries `language` column, and no srfm_before_entry_data callback injects it, so fresh installs (where the column is never created) are safe. The partial-entries RootLayout.js "language" reference is a docblock about visual styling, not the column.
…cks widgets
Adi's review: gating the payment-history stylesheet on has_block()/has_shortcode()
regressed the Elementor and Bricks payment-history widgets, whose content lives in
postmeta and so is never detected on wp_enqueue_scripts. The CSS then loaded only at
render() time (footer), so the dashboard rendered unstyled then restyled (FOUC).
1. HIGH — register the handle + declare it as a widget style dependency so both
builders enqueue it in the <head>:
- payment-history-shortcode.php: new register_assets() (wp_enqueue_scripts,
priority 1) registers the srfm-payment-history style + script handles
unconditionally; enqueue_assets() now enqueues by handle. The conditional
block/shortcode gate is unchanged, so the CSS still stays off unrelated pages.
- elementor/payment-history-widget.php: get_style_depends() → ['srfm-payment-history'].
- bricks/elements/payment-history-widget.php: enqueue_scripts() enqueues the handle.
The render()-time enqueue remains only as the last-resort FSE template-part /
block-widget fallback (footer CSS, acceptable there).
3. LOW — enqueue_assets() now takes an explicit $from_render flag instead of
sniffing doing_action('wp_enqueue_scripts') (which would also match a nested
do_shortcode() run inside a wp_enqueue_scripts callback). render() passes true.
Tests: added test_register_assets, Elementor test_get_style_depends, Bricks
test_enqueue_scripts; updated the render-fallback cases to the explicit-flag call
and simplified the on-hook helper (the gate is param-driven now).
…ice)
The Turnstile API script was enqueued with an invalid $args array:
[ false, 'defer' => true ]
which WordPress reads as unrecognized keys `0` and `defer`. On WordPress
7.0 this triggers a front-end "wp_enqueue_script was called incorrectly"
notice (Unrecognized keys in the $args parameter: 0, defer).
Replace it with the correct, documented form `[ 'strategy' => 'defer' ]`
(matching the existing hCaptcha enqueues). Behaviour is unchanged — the
script still loads deferred in the head — the invalid keys are removed.
Fixed in both enqueue sites: get_cf_turnstile_script() in
generate-form-markup.php and the inline-button captcha path in
inlinebutton-markup.php.
…older) The number field's `.srfm-input-content` wrapper set its top margin from `var(--srfm-input-field-margin-top)` while every other field's input uses the shared `srfm-input-margin-styles` expression `var(--srfm-input-label-gap, var(--srfm-input-field-margin-top))`. The number mixin simply never picked up the `--srfm-input-label-gap` fallback that was added later, so when a form's label gap differs from the size preset's field-margin-top, a number field placed next to another field (e.g. date) at 50% width sat a few px off — visible once labels become placeholders and the top margin becomes the field's leading space. Align the number wrapper to the same expression so it respects `--srfm-input-label-gap` like the rest. No change when the variable is unset (the fallback resolves to the same value as before). Pairs with the sureforms-pro date-field fix (date field now honors "Use label as placeholder"), which removes the larger label-height offset.
On the frontend, when a logged-in user who can view entries is on a page that contains a SureForms form, add an "Entries" node to the WP admin bar that deep-links to the Entries admin page pre-filtered to that form. With multiple forms on the page, a submenu lists one item per form and the parent links to the unfiltered Entries page. - Generate_Form_Markup::get_form_markup() (the single render choke point for both the block and the shortcode) records each rendered form ID in a static registry. The admin bar renders on wp_footer — after the_content — so the list is fully populated by the time the node is built. - add_entries_admin_bar_node() (admin_bar_menu, priority 100, mirroring the existing "Edit Form" node) builds the node(s), gated to is_admin_bar_showing() and Helper::current_user_can() (manage_options — the same capability the Entries admin page and REST endpoints require). Falls back to the singular form ID on Instant Form / single-form pages. - Deep link: admin.php?page=sureforms_entries#/?form=<id> — the format the React entries app's `form` search param already understands. No new class or loader change: the behavior lives on the already-registered Generate_Form_Markup singleton. Frontend-only (is_admin() guard). Closes #2988
…pages Show the admin-bar "Entries" deep-link only on a form's own Instant Form page (is_singular( SRFM_FORMS_POST_TYPE ) with enable_instant_form set — the same setting srfm_instant_form_redirect() gates the page on), not on every page that happens to render a form via the srfm/form block or [sureforms] shortcode. Removes the now-unused per-request rendered-form registry and the multi-form submenu; the node is always a single deep-link to the current form's filtered entries. Test updated to simulate the singular Instant Form page and assert the node is absent when Instant Form is disabled.
…-data Fix: entries search never matched submitted form data
…styling-none Fix: frontend TypeError on forms with default styles disabled
…uage-column Remove the entries language column (fixes failing migration + submission failures)
…e-wp7-notice-v2 Fix: correct Cloudflare Turnstile wp_enqueue_script $args (WP 7.0 notice)
…ies-stored-xss fix(security): stored XSS in entries admin view (CVE-2026-18406)
…epeater rows (#2993) Addresses the blocking review findings on #3005. H1 — repeater child keys bypassed the guard entirely. Pro submits rows as `repeaterKey[index][childKey]`, which PHP collapses into one top-level key, so the top-level-only loop never saw the child keys; `process_repeater_field()` copies them verbatim and they are label-decoded downstream. An attacker could copy a real repeater block_id out of page source and nest forged child keys inside it, re-obtaining exactly what #2993 describes. Rows are now walked; the allowlist already contained repeater children via the innerBlocks recursion. H2 — the guard rejected, and the error masked every other validation error. The allowlist is derived from post_content at submit time while the visitor's HTML was rendered earlier, so full-page caching or an editor-side block_id reassignment made a valid form unsubmittable. Worse, `field_errors` has no consumer and `reset()` in the caller promotes the first error to *the* message, hiding real per-field errors. Unknown keys are now dropped via a new `strip_unknown_field_keys()` called before validation, which meets the same security goal — the invented key never reaches storage, email or export. M2 — `@since 2.12.3` was a guessed version; now the `x.x.x` placeholder. M3 — the collector stored `sanitize_text_field( $block_id )` while the lookup used the raw id from `Helper::get_block_id_from_key()`. Any id the sanitiser altered was filed under a different string than the one looked up, making a legitimate field permanently unsubmittable. Both sides now use the raw id; these are comparison-only map keys and nothing is echoed from there. Also, from the non-blocking notes: the filter no longer advertises itself as a way to switch the check off, coerces a list return into a map and ignores a non-array return in favour of the walked set; only the pre-filter walk is memoised so a bad filter return cannot poison the request; and the collector takes a depth cap. M5 is resolved structurally — the strip now runs before `srfm_field_validation_data` is applied, so that filter's synthetic keys are no longer racing a memoised allowlist. Tests: repeater nesting (verified failing without the row walk), fail-open on an underivable block set, filter list/garbage coercion, and the existing non-alphanumeric block-id regression repointed at the new API.
…ed-payment fix(security): require a verified payment on payment-enabled forms (#2998)
…ate-form-markup-endpoint fix(security): gate generate-form-markup on capability and form type (#2995)
…ort-header fix(security): neutralise CSV formula injection in entries-export header (#2996)
…a-email-label
fix(security): escape {all_data} email label as text (#2991)
…-keys-against-form fix(security): reject undefined field keys on submission (#2993)
…e-field-validation fix(security): restrict nopriv unique check to the form's unique fields (#2997)
…994) Addresses the comment-level review findings on #3006. M1 — `@since 0.0.1` was factually wrong on `encode()`/`decode()`. Both are new symbols; calling either against any released SureForms is a fatal, and the two-line `@since` form documents a change to an existing element, not the introduction of one. `composer gen-stubs` copies these docblocks verbatim into the stubs Pro consumes, so a Pro dev reading `@since 0.0.1` would reasonably skip a version guard. Now a single `@since x.x.x`, with the rename note moved into the prose so the "behaviour unchanged since 0.0.1" fact is not lost. The alias `@since 0.0.1` tags are correct and are left alone. M2 — finished the encrypt/decrypt vocabulary sweep in files this PR already edited, where the stale wording undercuts the whole point of the rename: - `inc/fields/email-markup.php` — "Encrypted label" → "Encoded label", eight lines from an edit this PR already made. - `inc/abilities/entries/entry-parser.php` — "form data decryption" → "form data decoding"; the inline comment six lines below was already updated. - `inc/abilities/CLAUDE.md` — "decrypted form_data" → "decoded form_data"; the sibling line in `ABILITIES.md` was already updated. - `inc/rest-api.php` — the `srfm_entry_value` filter docblock told integrators they "may want to decrypt" the value. Reworded, and it now states plainly that nothing reaching that filter is encrypted by SureForms. Low — moved the escape-at-the-sink warning onto `get_field_label_from_key()`. That is what every unescaped sink actually calls, and it is strictly more dangerous than `decode()` alone: it additionally applies `html_entity_decode()`, which expands entities and so undoes any `htmlspecialchars()`-on-store defence. Left deliberately untouched: `AI_Auth` and the `openssl_*` paths (real AES), and the `test_encrypt*` method names, which test the alias and are accurately named. Comment-only change — no executable lines touched. PHPStan L9, PHPCS and PHP Insights clean; `Test_Helper` shows the same 4 pre-existing local failures before and after.
…-decrypt-encoding fix(security): rename misleading encrypt/decrypt to encode/decode (#2994)
…to dev-nr-2.12.3-sec
Dev to Next Release 2.12.3
Version Bump 2.12.3
Auto-generated by /i18n command on PR #3002
chore: update i18n translations
…ngelog Addresses the four code/metadata carry-overs @adi3890 raised on #3002. Docblocks (inc/ ships in the wp.org ZIP — .distignore excludes tests and src but not inc/, and the repo is private, so the ZIP is the first public exposure). Five docblocks narrated the pre-patch behaviour in enough detail to reconstruct the issue. Rewritten to state the invariant and why it must hold, dropping the "here is what used to happen" story, following the house style at inc/email/email-template.php:322: - inc/payments/front-end.php - inc/field-validation.php - inc/form-submit.php - inc/database/tables/entries.php - inc/generate-form-markup.php The behaviour and the reasons a future change must not relax these checks are preserved in full — only the exploit narrative is gone. Gruntfile.js: the @SInCE placeholder sweep globbed PHP only, so 20 files under src/ kept "x.x.x" through every release, and one built bundle (assets/js/payment-history.js) SHIPS with the placeholder — slightly wider than reported. Added src/**/*.js and assets/js/**/*.js. Also escaped the dots in the match pattern: /x.x.x/ig matched any character between the x's, which would rewrite incidental strings like "x1x2x". Latent before, but a real risk now that minified JS is in scope. readme.txt: - CVE-2026-18406 now has its own Fix: line with props to daroo and Wordfence, instead of being folded into the generic "Hardened security" improvement. - == Upgrade Notice == was an empty dangling section at EOF on a release carrying a 7.2 CVE; filled in for 2.12.3. Verified: PHPCS clean, PHPStan level 9 "No errors", Gruntfile parses.
…-overs Addresses @adi3890's review on #3018. MEDIUM — the sweep was fixed but never run. Ran it: 33 placeholders removed across 24 files. Verified the diff contains nothing but the substitution — every changed line is `@since x.x.x` -> `@since 2.12.3` and no other line differs. Gruntfile.js is deliberately skipped: it holds the search pattern itself. LOW — sweep scope. Accepted the point that `src` is the source of truth, but narrowing to `src/**/*.js` alone would have left two placeholders in the wordpress.org ZIP: inc/page-builders/elementor/assets/elementor-preview-styling.js:8 assets/js/payment-history.js:8 `inc/` is not in .distignore at all, and .distignore excludes only `assets/js/unminified`, so `assets/js/*.js` ships. Added `inc/**/*.js` and kept `assets/js/**/*.js`, with a comment recording why both are in scope so the next person does not narrow it back. Neither file was reachable by the previous PHP-only glob either, so both have shipped with a placeholder. Also reworded the new comment so it does not contain the literal placeholder: this task rewrites its own file, so the comment would have been substituted on the next bump. LOW — inc/field-validation.php:208 named the wrong consumer and the wrong verb. Confirmed get_known_field_block_ids() has exactly one caller, strip_unknown_field_keys() at :300, and that it unset()s the key at :315 rather than rejecting the submission. Docblock corrected and pointed at the note explaining why dropping beats rejecting. LOW — added the changelog line for the Helper::encrypt()/decrypt() deprecation. Confirmed the @deprecated tags exist (inc/helper.php:565, :604) and that sureforms-pro still calls the old names in 9 places, so add-on authors need the notice. Not in this PR: #3002's own body is still missing 4 changelog lines. That is a description edit, not code. Verified: 33/33 placeholders gone (only Gruntfile.js's own pattern remains), all swept JS parses, eslint clean on the swept src/ files, PHPCS clean on inc/field-validation.php, PHPStan level 9 "No errors".
The @SInCE sweep in the previous commit stamped @SInCE 2.12.3 on all 33 JS tags, but only the 8 in sanitizeEntryValue.js are new this cycle. The other 25 document code released earlier (each already carried x.x.x in the v2.12.2 tag), so 2.12.3 is wrong and @SInCE is the field add-on authors read for compatibility. Backfilled each tag with the version its code actually shipped in, dated by the earliest release tag containing the file (per-symbol where the file predates the tagged symbol): - Helpers.js: lockBlockSlugByBlockId 2.7.1, lockBlockSlugBySlug 2.7.0 - textarea.js Quill toolbar localization: 2.8.0 (file is v0.0.2) - payment-history block + assets: 2.8.0 - settings/migration/*: 2.11.0 - QuizEmptyState/upgrade-modal/styling-utils/preview-styling/elementor: 2.7.0 - SurveyEmptyState: 2.8.0, PartialEntriesEmptyState: 2.9.0 sanitizeEntryValue.js left at 2.12.3 (correct). No x.x.x reintroduced.
fix(2.12.3): release hygiene — docblock disclosure, @SInCE sweep, CVE changelog
Updated change log
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Syncs
brainstormforce/sureforms@master(private) into this public mirror.a57d81840..23f96be46— 563 commitspublic/mastervia a merge-cap, so it shows only real upstream changes — no internal-file deletions.Highlights
{all_data}email escaping,generate-form-markupendpoint capability gating, and a verified-payment requirement for payment-enabled forms@sincetag cleanup and docblock hygiene sweepStrip applied
Removed (not part of the public plugin):
.claude/,.scripts/git-hooks/,internal-docs/,docs/,ARCHITECTURE.md,COMPREHENSIVE_ANALYSIS.md,PRODUCT_ANALYSIS.md,TECHNICAL_OVERVIEW.md, all nestedCLAUDE.mdfiles,tests/play/specs/TODO.md, and 5 internal-only CI workflow files (push-to-deploy.yml,push-asset-readme-update.yml,release-tag-draft.yml,update-translations.yml,release-pr-template.yml), plus 3bin/release scripts.