diff --git a/CLAUDE.md b/CLAUDE.md
index e846156..d059fa5 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -720,6 +720,27 @@ in `~/git/foxhop-pillar/uwsgi/makepostsell/init.sls` for prod). Pattern mirrors
| `app.features.popout_player.enabled` | `request.popout_player_enabled` | True | Working |
| `app.features.karaoke.enabled` | `request.karaoke_enabled` | **False** | Broken (MPS-18) |
| `app.features.torrent.enabled` | `request.torrent_enabled` | **False** | Broken (MPS-19) |
+| `app.features.manual_tags.enabled` | `request.manual_tags_enabled` | **False** | Intentional — manual tags are ghost metadata (MPS-24) |
+
+## Tag Philosophy (MPS-24) — derive, don't hand-attach
+
+**Manual tags are "ghost metadata"** — operator-attached labels that
+are invisible to the humans and agents actually reading the page, and
+that drift out of sync with the words that matter. The blessed model:
+**tags are derived linguistically from the title + description** so
+humans and agents consume the same signal.
+
+- New products / edited title or description → `auto_hydrate_tags`
+ files the product into the shop's **existing** categories by content
+ (additive, idempotent, never creates).
+- Growing the category set → the **suggest** engine reads titles +
+ descriptions and proposes groupings the operator approves.
+- `request.manual_tags_enabled` (default **False**) hides the manual
+ add/apply UI on the product edit page and the bulk tagger; the
+ Suggest panel and the category overview stay. Flip
+ `MPS_FEATURES_MANUAL_TAGS_ENABLED=True` to restore manual tagging
+ "until further notice". Endpoints (`/p/{id}/tags`, bulk create/apply)
+ remain functional so the flip is instant and lossless.
When a flag is off:
1. Templates wrap UI in `{% if request.X_enabled %}` — section hidden
diff --git a/docs/tickets/mps-24.md b/docs/tickets/mps-24.md
index 6f70315..5121369 100644
--- a/docs/tickets/mps-24.md
+++ b/docs/tickets/mps-24.md
@@ -363,6 +363,23 @@ Tests (`test_functional.py::TestProductTagsSpa`):
Deferred (occasional click, not the hot path): AJAX-ifying the
"Suggest categories" link — still a full navigation by design.
+**Phase 2.8o — manual tags are ghost metadata: hide behind a flag**
+(shipped 2026-05-17): operator direction — stop hand-attaching tags
+("ghost metadata" invisible to humans/agents reading the page);
+derive them from title + description instead. New MPS-22-style kill
+switch `app.features.manual_tags.enabled` (`request.manual_tags_enabled`,
+**default False**, `MPS_FEATURES_MANUAL_TAGS_ENABLED` env, `True` in
+test.ini). When off: `product_edit.j2` hides the chip editor + comma
+field + `product_tags.js`, showing a "tags are derived from your
+title & description" note; `shop_tags.j2` hides the "Create a tag"
+form + the per-product apply (focus) section, showing a "How tags
+work" note — the **Suggest** panel + category overview stay (the
+derived path). Endpoints stay functional so the flip is instant +
+lossless ("until further notice"). Tests:
+`TestManualTagsKillSwitch` (fresh app, flag False — mirrors
+`TestKillSwitches`). Docs: CLAUDE.md "Tag Philosophy" + kill-switch
+matrix row.
+
**Phase 2.8n — auto-hydrate products into existing tags** (shipped
2026-05-17): operator wants new/edited products auto-filed into
existing categories without manual tagging.
diff --git a/make_post_sell/request_methods.py b/make_post_sell/request_methods.py
index b3d45c8..f7f161d 100644
--- a/make_post_sell/request_methods.py
+++ b/make_post_sell/request_methods.py
@@ -458,6 +458,23 @@ def includeme(config):
return val
return False
+ def add_manual_tags_enabled(request):
+ """MPS-24: manual tag UI kill switch. **Default OFF.**
+
+ Tags are "ghost metadata" the operator hand-attaches — invisible
+ to the humans and agents reading the page. We instead derive
+ tags linguistically from the title + description (auto-hydrate +
+ suggest engine). This flag hides the manual add/apply UI on the
+ product edit page and the bulk tagger "until further notice";
+ flip it on (MPS_FEATURES_MANUAL_TAGS_ENABLED=True) to restore.
+ """
+ val = request.app.get("features.manual_tags.enabled")
+ if isinstance(val, str):
+ return val.strip().lower() in ("1", "true", "yes", "on")
+ elif isinstance(val, bool):
+ return val
+ return False
+
# Feature toggles
config.add_request_method(
add_popout_player_enabled, "popout_player_enabled", reify=True
@@ -468,6 +485,9 @@ def includeme(config):
config.add_request_method(
add_torrent_enabled_global, "torrent_enabled", reify=True
)
+ config.add_request_method(
+ add_manual_tags_enabled, "manual_tags_enabled", reify=True
+ )
def add_git_hash(request):
"""Short git hash baked in at deploy time — used to cache-bust
diff --git a/make_post_sell/templates/product_edit.j2 b/make_post_sell/templates/product_edit.j2
index 4a4d287..6aa347b 100644
--- a/make_post_sell/templates/product_edit.j2
+++ b/make_post_sell/templates/product_edit.j2
@@ -495,14 +495,16 @@
- {# MPS-24: tags. Capability-driven (CLAUDE.md):
- - No JS: edit the comma-separated field, persists on "Save Settings"
- like every other field on this form (full page reload).
- - JS on: product_tags.js builds a chip editor from this field, hides
- the raw input, and POSTs each add/remove to /p/{id}/tags with
- X-Requested-With so the chip mutates in place — no full page
- reload. It rewrites the hidden field on every change so a later
- full "Save Settings" is a no-op, never a stale revert. #}
+ {# MPS-24: manual tagging is "ghost metadata" — invisible to the
+ humans and agents reading the page. Hidden by default
+ (request.manual_tags_enabled, see CLAUDE.md). Tags are derived
+ linguistically from the title + description instead and auto-
+ applied on save. Flip MPS_FEATURES_MANUAL_TAGS_ENABLED=True to
+ restore the manual chip editor below. #}
+ {% if request.manual_tags_enabled %}
+ {# Capability-driven: no-JS edits the comma field (persists on
+ "Save Settings"); product_tags.js turns it into a chip editor
+ that POSTs each add/remove to /p/{id}/tags. #}
+ Tags are derived from each product's title & description, + not hand-attached. Manual tags are ghost metadata: invisible + to the shoppers and agents actually reading the page, and they drift + out of sync with the words that matter. +
++ Instead: write a clear title and description. New and edited + products are automatically filed into your existing categories by + the language they contain. To grow your category set, click + Suggest categories below — it reads your catalog's titles and + descriptions and proposes real groupings you approve with one click. +
++ Manual tag creation and per-product apply are disabled until + further notice. The category list below stays available so you can + review, reorder, or remove categories. +
+