New rule at every selection stage: daily-finds drops such items at capture (categorical, overrides design/gift appeal; borderline cases skip too), build-bundle and cluster-bundles treat legacy alcohol finds as ineligible for new bundles, and build-review refuses to graduate them even on explicit slug. Existing finds stay published per forward-only decision; CLAUDE.md records the policy centrally. Claude-Session: https://claude.ai/code/session_01WZaczDJjL3xZ3u5spsN5AL
141 lines
9.8 KiB
Markdown
141 lines
9.8 KiB
Markdown
---
|
||
name: build-bundle
|
||
description: Draft a 'bundle' (themed editorial roundup of finds) for unique.rzen.dev. Selects find items from src/content/find/, groups them under a theme, and writes a new file at src/content/bundles/. Supports 'auto' (unattended, no confirmation) and 'weekly' (date-based catch of unbundled shortlist finds) modes. Use when the user runs /build-bundle, asks to "draft today's bundle", "build a roundup", "make a bundle", or "make a catch".
|
||
disable-model-invocation: true
|
||
---
|
||
|
||
# build-bundle
|
||
|
||
Author one editorial bundle (collection: `bundles`) that references existing find items. Each bundle has a theme, a short blurb, and an `items` array listing find slugs. The grid layout is rendered by the page template — this skill only writes the MDX.
|
||
|
||
Invoke from inside the `unique.rzen.dev` repo.
|
||
|
||
## Modes
|
||
|
||
- **Interactive (default when a user is present)**: propose the cluster, wait for sign-off (step 2), then write.
|
||
- **Autonomous (`auto` argument, or when running unattended — e.g. invoked by `/daily-pipeline`, a scheduled run, or any headless session)**: skip the confirmation gate. Apply the same quality bar, write directly, and record the would-have-been proposal in the log so the user can veto after the fact. Prefer the *tighter* theme when torn — an unattended run should err conservative.
|
||
- **Weekly catch (`weekly` argument)**: a date-based safety valve, not a themed bundle. Collect every `shortlist: true` find from the last 7 days that appears in **no** existing bundle's `items[]`; if fewer than 3, widen to all unbundled finds from the window. Title: "The week's catch — <Month D–D>". Blurb names the week, not a theme. Cap at 8 by shortlist rank. The hero floor (see below) applies here too, but widen rather than hold: if the window yields fewer than 2 hero-bearing items, pull in older unbundled hero-bearing finds until the floor is met — the weekly exists so finds don't stay invisible, so holding it defeats the point. Only if the *entire* unbundled pool can't muster 2 heroes, skip and report. If nothing qualifies, do nothing and report that. Purpose: no shortlisted find stays invisible for weeks awaiting a theme (finds are only reachable through bundles).
|
||
|
||
## Hero floor (applies to every bundle)
|
||
|
||
A bundle must contain **at least 2 items that have a hero image** (`hero.<ext>` next to the find's `index.mdx` — check with `ls src/content/find/<y>/<m>/<slug>/hero.*`). This is purely aesthetic: with 0–1 images the grid reads as a wall of text. Hero-less finds may still ride along freely — only the floor counts. Beyond the floor, **aim for ~4 heroes**: when choosing among equally good theme fits, prefer hero-bearing ones, and let bigger bundles (7–8 items) carry proportionally more images — 2 heroes in an 8-item grid still looks thin on the wide 4-column layout. If a proposed cluster can't meet even the floor of 2, **hold the bundle back** — don't pad with weak fits and don't write it anyway; the theme waits until enough hero-bearing finds emerge. In autonomous mode, log the held-back cluster (theme + items + which lack heroes) so the next run can revisit it.
|
||
|
||
## Inputs
|
||
|
||
- `src/content/find/<year>/<month>/<slug>/index.mdx` — the available pool of finds (folders are nested by date but slugs are globally unique; recurse with `find src/content/find -name index.mdx` or rely on the `find` content collection's id which is the leaf slug).
|
||
- `src/content/bundles/*.mdx` — existing bundles (used to detect already-bundled finds).
|
||
- Optional argument: a theme phrase (e.g. "single-virtue tools", "things-disguised-as-other-things").
|
||
- Optional argument: an explicit list of find slugs the user wants in the bundle.
|
||
|
||
## Procedure
|
||
|
||
### 1. Determine candidate pool
|
||
|
||
- **If the user provided explicit slugs (seed mode)** — usually the case when the user clicks the "Bundle this selection" button on `/find/`. Treat the slugs as a *seed*, not a final list.
|
||
1. Read each seed find's frontmatter (`tags`, `topics`, `subtitle`, `description`) and body.
|
||
2. Infer the through-line — the smallest phrase that captures what these finds have in common (a virtue, a use, a feeling, a structural pattern). Lean specific. Good: *"single-virtue tools"*, *"things that pour clean"*, *"objects that exist for no reason except someone wanted them to"*, *"vintage-leaning everyday carry"*. Bad: *"useful things"*, *"cool stuff"*.
|
||
3. Compute the **unbundled pool**: every find that does NOT appear in the `items[]` array of any existing `src/content/bundles/*.mdx` bundle. The seed slugs may or may not be in this pool — they're the seed regardless.
|
||
4. From the unbundled pool, find every entry that fits the inferred theme. Use frontmatter signals (tags, topics, subtitle) and body content. Don't reach: three weakly-fitting items is worse than zero. If a candidate's fit feels strained, drop it.
|
||
5. Combine `seed + qualifying unbundled matches`, deduped. Cap at **8 total** (per sizing notes below). If the combined list exceeds 8, drop the weakest fits — never drop a seed slug; if the seed itself is more than 8, ask the user which to drop.
|
||
6. Check the hero floor: if fewer than 2 items in the final list have a `hero.*` file, prefer swapping in hero-bearing theme matches; if the floor still can't be met, report that the bundle is held back (interactive: tell the user and let them decide; auto: hold and log).
|
||
7. Produce one cluster (theme phrase + final items list). Continue to step 2 to confirm before writing.
|
||
|
||
- **If the user provided a theme phrase only:** scan all finds, surface the 8–12 that best fit the theme.
|
||
|
||
- **If neither:** scan recent (last ~14 days) finds that are not yet referenced by any existing bundle, weighting `shortlist: true` finds first. From the unbundled pool, propose 1–3 thematic clusters of 5–10 finds each, with a candidate theme phrase per cluster. Only propose clusters that meet the hero floor. (In autonomous mode: pick the single strongest cluster that meets the floor and write it; if none does, skip the bundle this run and log why.)
|
||
|
||
Re-use is allowed — a find can appear in multiple bundles under different themes. But by default, prefer surfacing **unbundled** finds first; only reach for re-uses when the theme genuinely calls for them.
|
||
|
||
**Ineligible: tobacco- and alcohol-related finds.** The site no longer promotes either; a few legacy finds (wine, whisky, cocktail, bar items) predate the rule and stay published, but they must never be selected into a new bundle — not as expansion, not in the weekly catch, not even if the user's seed theme seems to invite them (flag it instead). Don't count them as "unbundled finds awaiting a home."
|
||
|
||
### 2. Confirm the cluster
|
||
|
||
**Autonomous mode skips this step** — apply the bar yourself, write, and put the cluster rationale in the log entry instead. Otherwise, show the proposed cluster(s) before writing anything. The exact format depends on which path step 1 took.
|
||
|
||
**Seed mode** (slugs given) — split the seed from the expansion so the user can see what the inferred theme pulled in:
|
||
|
||
```
|
||
Theme inferred from seed: "<theme phrase>"
|
||
|
||
Seed (your selection):
|
||
- <slug>: <name>
|
||
- ...
|
||
|
||
Expansion (unbundled finds matching the theme):
|
||
- <slug>: <name>
|
||
- ...
|
||
|
||
Final list (capped at 8): <N> items
|
||
```
|
||
|
||
**Multi-cluster mode** (no input):
|
||
|
||
```
|
||
Cluster A — "<theme phrase>"
|
||
- <slug>: <name>
|
||
- <slug>: <name>
|
||
- ...
|
||
|
||
Cluster B — "<theme phrase>"
|
||
...
|
||
```
|
||
|
||
In either format, annotate items that lack a hero image with `(no hero)` so the floor is visible at a glance.
|
||
|
||
Wait for the user to confirm, refine the theme phrase, or drop/swap items. Don't write anything until confirmed.
|
||
|
||
### 3. Write the bundle
|
||
|
||
File: `src/content/bundles/<YYYY-MM-DD>-<theme-slug>/index.mdx` (theme-slug = kebab-case of the theme phrase, lowercase, ~30 chars max).
|
||
|
||
Frontmatter:
|
||
|
||
```yaml
|
||
---
|
||
title: "<Theme phrased as a noun phrase, max ~60 chars>"
|
||
date: YYYY-MM-DD
|
||
tags: [daily-finds, <topic-tag-1>, <topic-tag-2>]
|
||
description: "<One-line description for meta tags. Reuse the title if nothing better.>"
|
||
blurb: "<One short editorial sentence; appears as the bundle's secondary line in the home feed.>"
|
||
items:
|
||
- <slug>
|
||
- <slug>
|
||
- ...
|
||
---
|
||
```
|
||
|
||
Body: a brief audience-facing intro paragraph (1–2 sentences max) — open with "Today's catch" or a close variant; name what links these finds; vary the wording day to day. **No** numbered candidates, no headings inside the body, no list items — the grid renders from `items[]`.
|
||
|
||
Example body:
|
||
|
||
```mdx
|
||
> Today's catch from a morning sweep across the daily feeds. Five small things, each one built around exactly one virtue — a single sound, a single diagnostic, a single disguise, a single object made, a single quiet joke.
|
||
```
|
||
|
||
### 4. Verify
|
||
|
||
- Confirm every slug in `items[]` exists at `src/content/find/<year>/<month>/<slug>/index.mdx` (use `find src/content/find -type d -name "<slug>"` to locate). If any is missing, abort and report — never reference a non-existent find.
|
||
- Confirm the hero floor: at least 2 of the `items[]` folders contain a `hero.*` file.
|
||
- Run `npm run build` to confirm the new bundle compiles.
|
||
|
||
### 5. Update the log
|
||
|
||
Append to `daily-finds.log.md`:
|
||
|
||
```
|
||
## YYYY-MM-DD — bundle: <theme>
|
||
|
||
Slug: <date>-<theme-slug>
|
||
Items referenced:
|
||
- <slug>
|
||
- <slug>
|
||
...
|
||
```
|
||
|
||
## Notes
|
||
|
||
- Default to 5 finds per bundle. Use 3 for tighter themes, up to 8 if the theme really supports it. Beyond 8 the grid stops being scannable. Whatever the size, the hero floor (≥2 hero-bearing items, aim for ~4) applies.
|
||
- The body is for the audience, not the user. Never write "pick one", "candidates", "today's pick" — those are internal terms.
|
||
- Never edit existing find files when building a bundle. The find body and metadata are stable; the bundle only references them.
|
||
- If a find that's referenced has been graduated to a review (its frontmatter has `promotedTo`), it can still appear in the bundle — the grid card simply links to the find page, which itself links forward to the review.
|