Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/easter-eggs.yml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion HANDOVER.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
34 changes: 34 additions & 0 deletions docs/easter-eggs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
46 changes: 46 additions & 0 deletions scripts/check-easter-eggs.sh
Original file line number Diff line number Diff line change
@@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Match canonical keywords literally in CI scan

The new CI check uses grep -l -- "$kw", which treats each keyword as a regex and matches substrings, so the guard can pass even when the canonical token is no longer present verbatim (for example, TEA matching inside another word, or future keywords with regex metacharacters like ./+). That weakens the single-source-of-truth contract because removing a real easter-egg token may not fail this workflow.

Useful? React with 👍 / 👎.

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"
36 changes: 29 additions & 7 deletions scripts/handoff/playtest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 ===
Expand Down Expand Up @@ -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
Expand Down
Loading