Add inverse_of tracking and admin question management

- Add inverse_of column support for question pair linking
- Enhance admin questions page with bulk operations
- Add DB migrations for inverse_of and FK indexes
- Update pack generator docs for inverse linking
- Improve Docker configs for admin app
This commit is contained in:
jjbrunton committed 2026-01-11 11:17:46 +00:00
1 parent efe93a3db4
commit df955812fe
22 files changed
+9951 -121

No files matched your search

+19 -5
View File
@@ -21,7 +21,9 @@ You are a content generation specialist for Sauci, a couples intimacy app. You c
3. **No time words**: Never tonight/now/today/right now
4. **Inverse requirement**: Every asymmetric question MUST have its inverse created
4. **Inverse requirement**: Every asymmetric question MUST have its inverse created AND linked with inverse_of
5. **Database linking**: Use the `inverse_of` column to link inverse pairs (inverse points to primary's UUID)
### Intensity Scale
@@ -39,20 +41,32 @@ You are a content generation specialist for Sauci, a couples intimacy app. You c
```
text: "Cook dinner together"
partner_text: NULL
inverse_of: NULL
```
**Use partner_text when roles differ:**
```
-- PRIMARY question (inverse_of = NULL)
id: uuid-1
text: "Give your partner oral"
partner_text: "Receive oral from your partner"
inverse_of: NULL
```
**CRITICAL - Always create the inverse:**
**CRITICAL - Always create the inverse AND link it:**
```
-- INVERSE question (inverse_of = primary's UUID)
id: uuid-2
text: "Receive oral from your partner"
partner_text: "Give your partner oral"
inverse_of: uuid-1
```
The `inverse_of` column ensures we can:
- Track which questions are pairs
- Calculate unique question count (not inflated by inverses)
- Display accurate pack sizes to users
### Avoid
- Cheesy language ("make love", "souls connecting")
@@ -70,9 +84,9 @@ partner_text: "Give your partner oral"
1. **Understand the brief**: Category, intensity range, pack position in progression
2. **Research existing content**: Check what packs exist, avoid overlap
3. **Brainstorm themes**: What specific activities fit this pack?
4. **Generate questions**: Mix symmetric and asymmetric, create all inverses
5. **Review**: Check all rules, remove duplicates, verify intensity
6. **Format**: Output as SQL or table format
4. **Generate questions**: Mix symmetric and asymmetric, create all inverses with inverse_of links
5. **Review**: Check all rules, remove duplicates, verify intensity, confirm inverse pairs are linked
6. **Format**: Output as SQL with inverse_of column properly set
## Output Format
+44 -5
View File
@@ -10,12 +10,13 @@ Fix questions in category: $ARGUMENTS
## Task Overview
Analyze questions across all packs in a category and apply fixes for issues across 5 dimensions:
Analyze questions across all packs in a category and apply fixes for issues across 6 dimensions:
1. **Text Phrasing** - Improve wording, remove wishy-washy language
2. **Audience Targeting** - Fix couple/initiator gender targeting
3. **Intensity Levels** - Correct misassigned intensity ratings
4. **Required Props** - Identify missing physical props/accessories
5. **Deletions** - Remove true duplicates, unsafe, or broken questions
5. **Inverse Linking** - Detect and link inverse question pairs using inverse_of column
6. **Deletions** - Remove true duplicates, unsafe, or broken questions
## Step 1: Parse Arguments
@@ -47,8 +48,8 @@ ORDER BY p.name;
```
```sql
-- Get questions for all packs in the category
SELECT q.id, q.text, q.partner_text, q.intensity, q.allowed_couple_genders, q.target_user_genders, q.required_props, p.name as pack_name
-- Get questions for all packs in the category (include inverse_of for pair detection)
SELECT q.id, q.text, q.partner_text, q.intensity, q.allowed_couple_genders, q.target_user_genders, q.required_props, q.inverse_of, p.name as pack_name
FROM questions q
JOIN question_packs p ON q.pack_id = p.id
WHERE p.category_id = '<category_id>' AND q.deleted_at IS NULL
@@ -138,10 +139,36 @@ Check if current intensity matches the activity:
**Rules:**
- Props are physical items/accessories, not body parts or acts
- Use lowercase, short, singular names (e.g., "blindfold", "remote vibrator")
- Try to avoid generics such as "toy" (unless the activity can be done with any toy)
- Reuse existing prop names from the catalog when possible
- If no props are required, set to null
### 3.5 Deletion Analysis
### 3.5 Inverse Linking Analysis
**Purpose:** Detect inverse question pairs and link them using the `inverse_of` database column.
**Detection Algorithm:**
1. For each asymmetric question (has partner_text), look for its inverse:
- Inverse exists if another question has: `text` = this question's `partner_text` AND `partner_text` = this question's `text`
2. Check if the `inverse_of` column is correctly set:
- One question in the pair should be the "primary" (inverse_of = NULL)
- The other should be the "inverse" (inverse_of = primary's UUID)
**Fixing Missing Links:**
- If inverse pair exists but neither has `inverse_of` set:
- Set the first one (by created_at) as primary (inverse_of = NULL)
- Set the second one's inverse_of = primary's UUID
- If inverse pair exists but both have `inverse_of = NULL`:
- Choose one as primary, link the other
- If a question has `inverse_of` pointing to a non-existent question:
- Find the actual inverse and fix the link, OR set to NULL if no inverse exists
**Important:**
- Only asymmetric questions (with partner_text) should have inverse pairs
- Symmetric questions (partner_text = NULL) should have inverse_of = NULL
- This fixes the database structure, NOT the question content
### 3.6 Deletion Analysis
**Deletion Categories:**
- `duplicate`: Same `text` AND same initiator as another question
@@ -188,6 +215,7 @@ Present findings grouped by fix type:
| Intensity fixes | X | X |
| Targeting fixes | X | X |
| Props fixes | X | X |
| Inverse linking fixes | X | X |
| Deletions | X | X |
| No changes needed | X | - |
@@ -198,11 +226,13 @@ For each question needing changes, show:
```
**Question ID:** <id>
**Current:** <text> / <partner_text or null>
**Current inverse_of:** <uuid or null>
**Issues:**
- Text: <issue description>
- Intensity: <current> -> <suggested> (<reason>)
- Targeting: <issue description>
- Props: <current> -> <suggested>
- Inverse Link: <missing/broken> - <inverse question id if found>
- DELETE: <category> - <reason>
**Fix:**
@@ -212,6 +242,7 @@ For each question needing changes, show:
- Couple Targets: <new allowed_couple_genders or null>
- Initiator Targets: <new target_user_genders or null>
- Required Props: <new required_props or null>
- inverse_of: <uuid or null>
```
## Step 5: Apply Fixes (if --apply flag)
@@ -237,6 +268,12 @@ UPDATE questions SET
required_props = ARRAY['...'] or NULL
WHERE id = '<question_id>';
-- Inverse linking fixes
-- Link inverse question to its primary
UPDATE questions SET inverse_of = '<primary_question_id>' WHERE id = '<inverse_question_id>';
-- Or clear a broken inverse_of link
UPDATE questions SET inverse_of = NULL WHERE id = '<question_id>';
-- Deletions (soft delete)
UPDATE questions SET deleted_at = NOW() WHERE id IN ('<id1>', '<id2>', ...);
```
@@ -265,7 +302,9 @@ If --apply is NOT set, output the SQL statements for manual review.
- DO prioritize safety issues (delete unsafe content first)
- DO preserve the core intent of questions when suggesting text fixes
- DO verify inverse pairs exist before flagging duplicates
- DO detect and link inverse pairs using the inverse_of column
- DON'T change questions that are already well-formed
- DON'T suggest overly restrictive targeting (prefer inclusive by default)
- DON'T create new prop names when existing ones match
- DON'T flag inverse pairs as duplicates (one's text = other's partner_text)
- DON'T forget to set inverse_of for detected inverse pairs - this ensures accurate unique question counts
+51 -13
View File
@@ -91,21 +91,33 @@ Follow these rules strictly:
- BAD: "Receive oral from your partner" (clinical)
- GOOD: "Let your partner pleasure you with their mouth" (enticing)
#### 3.4 Critical: Inverse Questions
#### 3.4 Critical: Inverse Questions and Database Linking
For EVERY asymmetric question, create its inverse:
For EVERY asymmetric question, create its inverse AND link them using the `inverse_of` column:
```
Original:
Original (PRIMARY - inverse_of = NULL):
id: <generated_uuid_1>
text: "Spank your partner"
partner_text: "Be spanked by your partner"
inverse_of: NULL
Inverse (MUST CREATE):
Inverse (SECONDARY - points to primary):
id: <generated_uuid_2>
text: "Be spanked by your partner"
partner_text: "Spank your partner"
inverse_of: <generated_uuid_1>
```
This ensures both partners are asked about both roles.
This ensures:
1. Both partners are asked about both roles
2. The database tracks which questions are inverses of each other
3. Accurate unique question counts (200 questions = 100 unique concepts with inverses)
**Database Schema:**
- `inverse_of` column: UUID referencing the primary question's ID
- Primary questions have `inverse_of = NULL`
- Inverse questions have `inverse_of = <primary_question_id>`
#### 3.5 Intensity Guidelines
@@ -195,15 +207,38 @@ VALUES (
)
RETURNING id;
-- Then insert questions (use returned pack_id)
INSERT INTO questions (pack_id, text, partner_text, intensity, allowed_couple_genders, target_user_genders, required_props) VALUES
('[pack_id]', 'Question text', 'Partner text or NULL', intensity, NULL, NULL, NULL),
('[pack_id]', 'Blowjob question', 'Partner text', 3, ARRAY['male+male', 'female+male'], ARRAY['female'], NULL),
('[pack_id]', 'Toy question', 'Partner text', 4, NULL, NULL, ARRAY['remote vibrator']),
-- ... all questions
-- Then insert questions with inverse_of linking
-- For inverse pairs: insert primary first (inverse_of = NULL), then inverse pointing to primary
-- Generate UUIDs for inverse pairs upfront
-- Pair 1: Primary and its inverse
WITH pair1_primary AS (
INSERT INTO questions (pack_id, text, partner_text, intensity, inverse_of)
VALUES ('[pack_id]', 'Spank your partner', 'Be spanked by your partner', 3, NULL)
RETURNING id
)
INSERT INTO questions (pack_id, text, partner_text, intensity, inverse_of)
SELECT '[pack_id]', 'Be spanked by your partner', 'Spank your partner', 3, id
FROM pair1_primary;
-- Symmetric questions (no inverse_of needed)
INSERT INTO questions (pack_id, text, partner_text, intensity, allowed_couple_genders, target_user_genders, required_props, inverse_of) VALUES
('[pack_id]', 'Symmetric question text', NULL, 2, NULL, NULL, NULL, NULL);
-- Or bulk insert with explicit UUIDs for pairs:
INSERT INTO questions (id, pack_id, text, partner_text, intensity, inverse_of) VALUES
-- Pair 1
('uuid-primary-1', '[pack_id]', 'Give your partner a massage', 'Receive a massage from your partner', 2, NULL),
('uuid-inverse-1', '[pack_id]', 'Receive a massage from your partner', 'Give your partner a massage', 2, 'uuid-primary-1'),
-- Pair 2
('uuid-primary-2', '[pack_id]', 'Blindfold your partner', 'Be blindfolded by your partner', 3, NULL),
('uuid-inverse-2', '[pack_id]', 'Be blindfolded by your partner', 'Blindfold your partner', 3, 'uuid-primary-2'),
-- Symmetric (no pair)
(gen_random_uuid(), '[pack_id]', 'Cook dinner together', NULL, 1, NULL);
```
**Field notes:**
- `inverse_of`: NULL for primary/symmetric questions, UUID of primary question for inverses
- `allowed_couple_genders`: NULL for all couples, or array like `ARRAY['male+male', 'female+male']`
- `target_user_genders`: NULL for any initiator, or array like `ARRAY['female']`
- `required_props`: NULL if no props needed, or array like `ARRAY['blindfold', 'ice cubes']`
@@ -213,13 +248,14 @@ Also provide a summary table:
| Metric | Value |
|--------|-------|
| Total questions | X |
| Unique question concepts | X (total - inverse count) |
| Intensity 1 | X |
| Intensity 2 | X |
| Intensity 3 | X |
| Intensity 4 | X |
| Intensity 5 | X |
| Symmetric (null partner_text) | X |
| Asymmetric (with inverses) | X pairs |
| Asymmetric pairs (with inverse_of links) | X pairs (X questions) |
| With couple targeting | X |
| With initiator targeting | X |
| With required props | X |
@@ -236,10 +272,12 @@ Also provide a summary table:
- DO ask clarifying questions if the pack concept is unclear
- DO check existing packs in the category first
- DO create inverse questions for ALL asymmetric content
- DO create inverse questions for ALL asymmetric content AND link them with inverse_of
- DO vary sentence structure and openers
- DO use the inverse_of column to link inverse pairs (inverse points to primary's UUID)
- DON'T use cheesy or cliche language
- DON'T assume heterosexual couples
- DON'T skip the inverse question requirement
- DON'T forget to set inverse_of - this is how we track unique question concepts
- DON'T use interview-style questions ("Would you like to...")
- DON'T treat inverse pairs as duplicates - they are REQUIRED (one's `text` = other's `partner_text`)
+34 -10
View File
@@ -10,12 +10,13 @@ Review and validate questions: $ARGUMENTS
## Task Overview
Analyze questions for issues and suggest fixes across 5 dimensions:
Analyze questions for issues and suggest fixes across 6 dimensions:
1. **Text Phrasing** - Improve wording, remove wishy-washy language
2. **Audience Targeting** - Fix couple/initiator gender targeting
3. **Intensity Levels** - Correct misassigned intensity ratings
4. **Required Props** - Identify missing physical props/accessories
5. **Deletions** - Flag duplicates, redundant, or broken questions
5. **Inverse Linking** - Detect and link inverse question pairs using inverse_of column
6. **Deletions** - Flag duplicates, redundant, or broken questions
## Step 1: Parse Arguments
@@ -29,18 +30,18 @@ Extract from $ARGUMENTS:
Query the database:
```sql
-- If pack name provided:
SELECT q.id, q.text, q.partner_text, q.intensity, q.allowed_couple_genders, q.target_user_genders, q.required_props
-- If pack name provided (include inverse_of for pair detection):
SELECT q.id, q.text, q.partner_text, q.intensity, q.allowed_couple_genders, q.target_user_genders, q.required_props, q.inverse_of
FROM questions q
JOIN question_packs p ON q.pack_id = p.id
WHERE p.name ILIKE '%<pack_name>%'
WHERE p.name ILIKE '%<pack_name>%' AND q.deleted_at IS NULL
ORDER BY q.created_at
LIMIT <limit>;
-- If pack ID provided:
SELECT id, text, partner_text, intensity, allowed_couple_genders, target_user_genders, required_props
SELECT id, text, partner_text, intensity, allowed_couple_genders, target_user_genders, required_props, inverse_of
FROM questions
WHERE pack_id = '<pack_id>'
WHERE pack_id = '<pack_id>' AND deleted_at IS NULL
ORDER BY created_at
LIMIT <limit>;
```
@@ -138,7 +139,20 @@ Check if current intensity matches the activity:
- Reuse existing prop names from the catalog when possible
- If no props are required, set to null
### 4.5 Deletion Analysis
### 4.5 Inverse Linking Analysis
**Purpose:** Detect inverse question pairs and link them using the `inverse_of` database column.
**Detection:**
1. For each asymmetric question (has partner_text), look for its inverse:
- Inverse exists if another question has: `text` = this question's `partner_text` AND `partner_text` = this question's `text`
2. Check if the `inverse_of` column is correctly set
**Fixes:**
- If pair exists but not linked: set inverse_of on the second question to point to the first
- Symmetric questions should have inverse_of = NULL
### 4.6 Deletion Analysis
**Deletion Categories:**
- `duplicate`: Same core action as another question
@@ -184,6 +198,7 @@ Present findings in a structured format:
| Intensity fixes | X |
| Targeting fixes | X |
| Props fixes | X |
| Inverse linking fixes | X |
| Deletions suggested | X |
| No changes needed | X |
@@ -196,12 +211,14 @@ For each question needing changes:
**Current Text:** <text>
**Current Partner Text:** <partner_text or null>
**Current Intensity:** <intensity>
**Current inverse_of:** <uuid or null>
Issues Found:
- [ ] Text: <description of issue>
- [ ] Intensity: <current> -> <suggested> (<reason>)
- [ ] Targeting: <description>
- [ ] Props: <current> -> <suggested>
- [ ] Inverse Link: <missing/broken> - <inverse question id if found>
- [ ] DELETE: <category> - <reason>
Suggested Fix:
@@ -211,6 +228,7 @@ Suggested Fix:
- Couple Targets: <new allowed_couple_genders or null>
- Initiator Targets: <new target_user_genders or null>
- Required Props: <new required_props or null>
- inverse_of: <uuid or null>
```
### SQL to Apply Fixes
@@ -236,8 +254,11 @@ UPDATE questions SET
required_props = ARRAY[<new_props>]
WHERE id = '<question_id>';
-- Deletions
DELETE FROM questions WHERE id IN ('<id1>', '<id2>', ...);
-- Inverse linking fixes
UPDATE questions SET inverse_of = '<primary_question_id>' WHERE id = '<inverse_question_id>';
-- Deletions (soft delete)
UPDATE questions SET deleted_at = NOW() WHERE id IN ('<id1>', '<id2>', ...);
```
## Example Usage
@@ -254,6 +275,9 @@ DELETE FROM questions WHERE id IN ('<id1>', '<id2>', ...);
- DO group similar issues together for batch fixes
- DO prioritize safety issues (delete unsafe content first)
- DO preserve the core intent of questions when suggesting text fixes
- DO detect and link inverse pairs using the inverse_of column
- DON'T change questions that are already well-formed
- DON'T suggest overly restrictive targeting (prefer inclusive by default)
- DON'T create new prop names when existing ones match
- DON'T flag inverse pairs as duplicates (one's text = other's partner_text)
- DON'T forget to set inverse_of for detected inverse pairs