diff --git a/.github/workflows/easter-eggs.yml b/.github/workflows/easter-eggs.yml new file mode 100644 index 0000000..30a207b --- /dev/null +++ b/.github/workflows/easter-eggs.yml @@ -0,0 +1,26 @@ +name: easter-egg keywords + +# Fails if any keyword in docs/easter-eggs.md's canonical keyword block is +# absent from every v1-modern/src/scripts/*.narrat file. Prevents the silent +# drift described in issue #6. +# +# This workflow does not use any untrusted input (no github.event.* in run: +# commands); it only runs a checked-in script. + +on: + pull_request: + paths: + - 'docs/easter-eggs.md' + - 'v1-modern/src/scripts/**.narrat' + - 'scripts/check-easter-eggs.sh' + - '.github/workflows/easter-eggs.yml' + push: + branches: [main] + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Verify every canonical keyword appears in some narrat script + run: bash scripts/check-easter-eggs.sh diff --git a/HANDOVER.md b/HANDOVER.md index 79e09df..8c29fae 100644 --- a/HANDOVER.md +++ b/HANDOVER.md @@ -15,7 +15,7 @@ You are the lead coder continuing development of **PNK Forever**, an anniversary - **Volume 2 — Modern Edition**: `v1-modern/` — a Narrat v3 visual novel with real backgrounds, sprites, and 5 chapters. This is where active development happens. - **Main characters**: P. (Phoenix — a bird-cat), K. (Ehecatl — a peacock-tailed shiba dog). - **Tone**: warm, witty, intimate. Easter-egg keywords in ALL-CAPS feel like inside jokes. -- **Sacred easter-egg keywords that must keep appearing**: MANGO, TEA, CHOCOLATE, KITE, LOVE, FLY, TIGER, SNAKE, ZODIAC, FUTURE, NECKLACE, BROMPTON, JAFFA, JAPAN, and the discount code `PNK-n3zk7MAMBG-GIFT`. +- **Sacred easter-egg keywords that must keep appearing**: see the canonical list in [`docs/easter-eggs.md`](docs/easter-eggs.md#canonical-keyword-list). That file is the single source of truth — do not re-list keywords here or anywhere else (playtest and CI read from it at runtime; duplicating invites silent drift). ### 2. Repo layout diff --git a/docs/easter-eggs.md b/docs/easter-eggs.md index 57809bb..f1fe86f 100644 --- a/docs/easter-eggs.md +++ b/docs/easter-eggs.md @@ -12,6 +12,40 @@ This gamification of reality creates a unique experience where: --- +## Canonical Keyword List + +This is the authoritative machine-readable keyword list. It is parsed at +runtime by `scripts/handoff/playtest.sh` (presence check in rendered page +content) and by `scripts/check-easter-eggs.sh` (CI check that every keyword +appears in at least one `v1-modern/src/scripts/*.narrat` file). + +**Format:** one keyword per line inside the fenced block below. Do not +reformat, add prose, or split the block — the parsers look for +```` ```keywords ```` as the block fence. + +```keywords +MANGO +TEA +CHOCOLATE +KITE +LOVE +FLY +TIGER +SNAKE +ZODIAC +FUTURE +NECKLACE +BROMPTON +JAFFA +JAPAN +PNK-n3zk7MAMBG-GIFT +``` + +When adding a new easter egg, append its keyword here *and* reference this +list from any runtime or docs that would otherwise hard-code it. + +--- + ## IFTTT Integration System All Easter eggs use IFTTT (If This Then That) webhooks to trigger real-world actions. diff --git a/scripts/check-easter-eggs.sh b/scripts/check-easter-eggs.sh new file mode 100755 index 0000000..5555fe8 --- /dev/null +++ b/scripts/check-easter-eggs.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# check-easter-eggs.sh — fails if any keyword from docs/easter-eggs.md's +# canonical list is absent from every v1-modern/src/scripts/*.narrat file. +# +# Used by the easter-eggs CI workflow and can be run locally as a pre-commit +# check. Part of the single-source-of-truth contract established in issue #6. + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +KEYWORDS_FILE="$REPO_ROOT/docs/easter-eggs.md" +NARRAT_DIR="$REPO_ROOT/v1-modern/src/scripts" + +if [[ ! -f "$KEYWORDS_FILE" ]]; then + echo "ERROR: $KEYWORDS_FILE not found" >&2 + exit 2 +fi + +# Parse keywords from the fenced ```keywords ... ``` block. +keywords=$(awk ' + /^```keywords$/ { inblock=1; next } + /^```$/ && inblock { exit } + inblock && NF > 0 { print } +' "$KEYWORDS_FILE") + +if [[ -z "$keywords" ]]; then + echo "ERROR: no keywords parsed from $KEYWORDS_FILE (missing or malformed \`\`\`keywords block)" >&2 + exit 2 +fi + +total=$(printf '%s\n' "$keywords" | grep -c .) +missing=() +while IFS= read -r kw; do + [[ -z "$kw" ]] && continue + if ! grep -l -- "$kw" "$NARRAT_DIR"/*.narrat >/dev/null 2>&1; then + missing+=("$kw") + fi +done <<<"$keywords" + +if (( ${#missing[@]} > 0 )); then + echo "ERROR: these keywords from $KEYWORDS_FILE are absent from every $NARRAT_DIR/*.narrat:" >&2 + printf ' - %s\n' "${missing[@]}" >&2 + exit 1 +fi + +echo "OK: all $total easter-egg keywords present in at least one narrat script" diff --git a/scripts/handoff/playtest.sh b/scripts/handoff/playtest.sh index 97a9a71..8a0e5e1 100755 --- a/scripts/handoff/playtest.sh +++ b/scripts/handoff/playtest.sh @@ -368,14 +368,23 @@ async function runPlaytest() { } // === Phase 7: Easter egg keywords === + // Keywords are injected via EASTER_EGG_KEYWORDS env var. The single + // source of truth is docs/easter-eggs.md (see the ```keywords``` fenced + // block). If the env var is unset the check is skipped with a warning. const pageContent = await page.content(); - const eggs = ['mango', 'tea', 'chocolate', 'kite', 'love', 'fly']; - const foundEggs = eggs.filter(e => pageContent.toLowerCase().includes(e)); - if (foundEggs.length >= 3) { - findings.ready_criteria.easter_eggs_present = true; - findings.positives.push(`Easter egg keywords present: ${foundEggs.join(', ')}`); + const rawKeywords = process.env.EASTER_EGG_KEYWORDS || ''; + const eggs = rawKeywords.split(',').map(k => k.trim()).filter(Boolean); + if (eggs.length === 0) { + findings.warnings.push('EASTER_EGG_KEYWORDS env var empty — keyword presence check skipped'); } else { - findings.improvements.push(`Only ${foundEggs.length}/6 easter egg keywords found - may need more scenes played`); + const foundEggs = eggs.filter(e => pageContent.toLowerCase().includes(e.toLowerCase())); + const threshold = Math.max(3, Math.floor(eggs.length / 3)); + if (foundEggs.length >= threshold) { + findings.ready_criteria.easter_eggs_present = true; + findings.positives.push(`Easter egg keywords present: ${foundEggs.join(', ')}`); + } else { + findings.improvements.push(`Only ${foundEggs.length}/${eggs.length} easter egg keywords found (threshold ${threshold}) - may need more scenes played`); + } } // === Phase 8: Console errors === @@ -403,7 +412,20 @@ console.log(JSON.stringify(results, null, 2)); EOTEST echo "Running automated playtest..." -RESULTS=$(PREVIEW_URL="$URL" node test.mjs 2>/tmp/playtest-stderr.log) + +# Parse canonical easter-egg keywords from docs/easter-eggs.md — single +# source of truth per issue #6. Passed as comma-separated env var to test.mjs. +EASTER_EGG_KEYWORDS=$(awk ' + /^```keywords$/ { inblock=1; next } + /^```$/ && inblock { exit } + inblock && NF > 0 { printf "%s%s", sep, $0; sep="," } +' "$REPO_ROOT/docs/easter-eggs.md") + +if [[ -z "$EASTER_EGG_KEYWORDS" ]]; then + echo "WARN: no keywords parsed from docs/easter-eggs.md — easter-egg check will be skipped" +fi + +RESULTS=$(PREVIEW_URL="$URL" EASTER_EGG_KEYWORDS="$EASTER_EGG_KEYWORDS" node test.mjs 2>/tmp/playtest-stderr.log) STDERR_OUTPUT=$(cat /tmp/playtest-stderr.log 2>/dev/null || echo "") if [[ -n "$STDERR_OUTPUT" ]]; then