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
128 changes: 128 additions & 0 deletions _tools/migrate_concept_journeys.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
#!/usr/bin/env python3
"""Migration: convert concept journey data from concepts.json into unified journey JSON.

Reads content/meta/concepts.json, finds every concept with non-empty `journey_stops`,
and writes a journey JSON file at content/meta/journeys/concept/{concept_id}.json.

Idempotent — running twice cleanly overwrites existing files.

Usage (from repo root):
python _tools/migrate_concept_journeys.py
"""
import json
import os
import sys
from pathlib import Path

# Resolve repo root from this file's location (_tools/migrate_concept_journeys.py)
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / '_tools'))

from content_writer import save_journey


def load_concepts():
"""Load concepts.json and return the list of concept dicts."""
path = REPO / 'content' / 'meta' / 'concepts.json'
with open(path, 'r', encoding='utf-8') as f:
return json.load(f)


def build_concept_journey(concept):
"""Convert a concept dict with journey_stops into unified journey format."""
stops_raw = concept.get('journey_stops', [])
if not stops_raw:
return None

cid = concept['id']
title = concept['title']

# Ensure description is non-empty (required by save_journey)
description = concept.get('description', '')
if not description.strip():
description = f'{title} in Scripture'

depth = 'long' if len(stops_raw) >= 10 else 'medium' if len(stops_raw) >= 6 else 'short'

# Build hero_image_url from first image if available
images = concept.get('images')
hero_image_url = None
if images and isinstance(images, list) and len(images) > 0:
first_img = images[0]
if isinstance(first_img, dict):
hero_image_url = first_img.get('url')

# Build tags from related IDs
tags = []
for wid in concept.get('word_study_ids', []):
tags.append({'type': 'word_study', 'id': wid})
for pid in concept.get('prophecy_chain_ids', []):
tags.append({'type': 'prophecy_chain', 'id': pid})
for pid in concept.get('people_tags', []):
tags.append({'type': 'person', 'id': pid})
for tag in concept.get('tags', []):
tags.append({'type': 'theme', 'id': tag})

# Build stops
stops = []
for i, stop in enumerate(stops_raw):
stops.append({
'stop_order': i + 1,
'stop_type': 'regular',
'label': stop['label'],
'ref': stop['ref'],
'book_id': stop['book'],
'chapter_num': stop['chapter'],
'verse_start': None,
'verse_end': None,
'development': stop.get('development', ''),
'what_changes': stop.get('what_changes', ''),
'linked_journey_id': None,
'linked_journey_intro': None,
'bridge_to_next': '[BRIDGE TO AUTHOR]' if i < len(stops_raw) - 1 else None,
})

return {
'id': cid,
'journey_type': 'concept',
'lens_id': 'theological',
'title': title,
'subtitle': None,
'description': description,
'depth': depth,
'sort_order': 0,
'person_id': None,
'concept_id': cid,
'era': None,
'hero_image_url': hero_image_url,
'tags': tags,
'stops': stops,
}


def main():
print('=== Migrating concept journeys to unified format ===\n')

concepts = load_concepts()
migrated = 0
skipped = 0

for concept in concepts:
stops = concept.get('journey_stops', [])
if not stops:
skipped += 1
continue

journey_dict = build_concept_journey(concept)
if journey_dict is None:
skipped += 1
continue

save_journey('concept', journey_dict)
migrated += 1

print(f'\n=== Done: {migrated} concept journeys migrated, {skipped} skipped ===')


if __name__ == '__main__':
main()
131 changes: 131 additions & 0 deletions _tools/migrate_person_journeys.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#!/usr/bin/env python3
"""Migration: convert person journey data from people.json into unified journey JSON.

Reads content/meta/people.json, finds every person with a non-empty `journey`
array, and writes a journey JSON file at content/meta/journeys/person/{person_id}.json.

Idempotent — running twice cleanly overwrites existing files.

Usage (from repo root):
python _tools/migrate_person_journeys.py
"""
import json
import os
import sys
from pathlib import Path

# Resolve repo root from this file's location (_tools/migrate_person_journeys.py)
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / '_tools'))

from content_writer import save_journey


def load_people():
"""Load people.json and return the list of person dicts."""
path = REPO / 'content' / 'meta' / 'people.json'
with open(path, 'r', encoding='utf-8') as f:
data = json.load(f)
return data['people']


def parse_chapters(raw):
"""Normalise chapters field — could be a list or a JSON string."""
if raw is None:
return []
if isinstance(raw, str):
try:
parsed = json.loads(raw)
if isinstance(parsed, list):
return parsed
except (json.JSONDecodeError, TypeError):
return []
if isinstance(raw, list):
return raw
return []


def build_person_journey(person):
"""Convert a person dict with journey stages into unified journey format."""
journey = person.get('journey', [])
if not journey:
return None

pid = person['id']
name = person['name']
role = person.get('role')

# Ensure description is non-empty (required by save_journey)
description = person.get('bio') or person.get('summary') or ''
if not description.strip():
parts = [name]
if role:
parts.append(role)
description = ' — '.join(parts)

depth = 'long' if len(journey) >= 8 else 'medium' if len(journey) >= 5 else 'short'

stops = []
for i, stage in enumerate(journey):
chapters = parse_chapters(stage.get('chapters'))
chapter_num = chapters[0] if chapters else None

stops.append({
'stop_order': i + 1,
'stop_type': 'regular',
'label': stage['stage'],
'ref': stage['verse_ref'],
'book_id': stage['book_dir'],
'chapter_num': chapter_num,
'verse_start': None,
'verse_end': None,
'development': stage.get('summary', ''),
'what_changes': stage.get('theme', ''),
'linked_journey_id': None,
'linked_journey_intro': None,
'bridge_to_next': '[BRIDGE TO AUTHOR]' if i < len(journey) - 1 else None,
})

return {
'id': pid,
'journey_type': 'person',
'lens_id': 'biographical',
'title': name,
'subtitle': role,
'description': description,
'depth': depth,
'sort_order': 0,
'person_id': pid,
'concept_id': None,
'era': person.get('era'),
'hero_image_url': person.get('image_url'),
'tags': [],
'stops': stops,
}


def main():
print('=== Migrating person journeys to unified format ===\n')

people = load_people()
migrated = 0
skipped = 0

for person in people:
journey = person.get('journey', [])
if not journey:
continue

journey_dict = build_person_journey(person)
if journey_dict is None:
skipped += 1
continue

save_journey('person', journey_dict)
migrated += 1

print(f'\n=== Done: {migrated} person journeys migrated, {skipped} skipped ===')


if __name__ == '__main__':
main()
4 changes: 2 additions & 2 deletions app/assets/db-manifest.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"content_hash": "d1d5fbec22de929b",
"build_time": "2026-04-16T17:05:55.314615Z"
"content_hash": "750e7dbf67dabf92",
"build_time": "2026-04-16T17:20:10.368400Z"
}
Loading
Loading