Parent epic: #1379
Track: Code
Phase: 1 — Foundation
Dependency protocol
Blocked by: None (first issue in Phase 1 foundation)
Blocks: #1405 (build pipeline), #1406 (schema validator)
IMPORTANT: When this issue is complete, open a PR. Wait for merge to master before starting dependent issues. If you are Claude Code, do not proceed to dependent tasks in the same run.
Goal
Create the unified journeys, journey_stops, and journey_tags tables in the SQLite schema. Also retire the legacy concepts and people_journeys tables.
What to build
1. Add new schema to _tools/build_sqlite_loaders.py
Find the schema definition section (where tables like chapters, sections, scholars are defined) and add:
# Unified journeys table — covers person, concept, and thematic journeys
cur.execute('''
CREATE TABLE IF NOT EXISTS journeys (
id TEXT PRIMARY KEY,
journey_type TEXT NOT NULL CHECK (journey_type IN ('person', 'concept', 'thematic')),
title TEXT NOT NULL,
subtitle TEXT,
description TEXT NOT NULL,
lens_id TEXT,
depth TEXT CHECK (depth IN ('short', 'medium', 'long')),
sort_order INTEGER DEFAULT 0,
person_id TEXT,
concept_id TEXT,
era TEXT,
tags TEXT,
hero_image_url TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
''')
cur.execute('CREATE INDEX IF NOT EXISTS idx_journeys_type ON journeys(journey_type)')
cur.execute('CREATE INDEX IF NOT EXISTS idx_journeys_lens ON journeys(lens_id)')
cur.execute('''
CREATE TABLE IF NOT EXISTS journey_stops (
id INTEGER PRIMARY KEY AUTOINCREMENT,
journey_id TEXT NOT NULL REFERENCES journeys(id) ON DELETE CASCADE,
stop_order INTEGER NOT NULL,
stop_type TEXT NOT NULL CHECK (stop_type IN ('regular', 'linked_journey')),
label TEXT,
ref TEXT,
book_id TEXT,
chapter_num INTEGER,
verse_start INTEGER,
verse_end INTEGER,
development TEXT,
what_changes TEXT,
linked_journey_id TEXT,
linked_journey_intro TEXT,
bridge_to_next TEXT,
UNIQUE (journey_id, stop_order)
)
''')
cur.execute('CREATE INDEX IF NOT EXISTS idx_stops_journey ON journey_stops(journey_id)')
cur.execute('''
CREATE TABLE IF NOT EXISTS journey_tags (
journey_id TEXT NOT NULL REFERENCES journeys(id) ON DELETE CASCADE,
tag_type TEXT NOT NULL CHECK (tag_type IN ('person', 'place', 'theme', 'word_study', 'prophecy_chain')),
tag_id TEXT NOT NULL,
PRIMARY KEY (journey_id, tag_type, tag_id)
)
''')
cur.execute('CREATE INDEX IF NOT EXISTS idx_journey_tags_target ON journey_tags(tag_type, tag_id)')
2. Remove the legacy concepts and people_journeys table creation
Search _tools/build_sqlite_loaders.py for:
CREATE TABLE IF NOT EXISTS concepts
CREATE TABLE IF NOT EXISTS people_journeys
Delete these CREATE statements entirely. Audit issue #1403 has the full inventory of references.
Evaluate people_legacy_refs — if PersonDetailScreen still references it (check hooks usePersonJourney.ts), keep it for now; it's out of scope for this epic.
3. Create content/meta/journey-lenses.json
[
{ "id": "theological", "name": "Theological Development", "sort": 1 },
{ "id": "biographical", "name": "Biographical Arc", "sort": 2 },
{ "id": "narrative", "name": "Narrative Arc", "sort": 3 },
{ "id": "questions", "name": "Theological Questions", "sort": 4 },
{ "id": "comparative", "name": "Comparative Studies", "sort": 5 },
{ "id": "cultural", "name": "Cultural Threads", "sort": 6 },
{ "id": "reading_paths", "name": "Reading Paths & Intros", "sort": 7 },
{ "id": "christological", "name": "Christological", "sort": 8 },
{ "id": "liturgical", "name": "Liturgical & Devotional", "sort": 9 },
{ "id": "hard_sayings", "name": "Hard Sayings & Apologetics", "sort": 10 },
{ "id": "geographic", "name": "Geographic & Spatial", "sort": 11 },
{ "id": "historical", "name": "Historical Context", "sort": 12 },
{ "id": "application", "name": "Life Application", "sort": 13 },
{ "id": "ane", "name": "Comparative Religion / ANE", "sort": 14 }
]
4. Create content/meta/journeys/ directory structure
Create empty subdirectories:
content/meta/journeys/thematic/
content/meta/journeys/concept/
content/meta/journeys/person/
Add a .gitkeep in each so git tracks them.
Acceptance criteria
Files to modify
_tools/build_sqlite_loaders.py — add new tables, remove retired ones
content/meta/journey-lenses.json — new file
content/meta/journeys/{thematic,concept,person}/.gitkeep — new files
Workflow
git checkout -b feature/1379-schema-foundation
# make changes
python _tools/build_sqlite.py # must succeed
git add -A
git commit -m "Schema: add unified journeys tables, retire concepts and people_journeys (#1379)"
git push
# open PR to master, wait for merge
Parent epic: #1379
Track: Code
Phase: 1 — Foundation
Dependency protocol
Blocked by: None (first issue in Phase 1 foundation)
Blocks: #1405 (build pipeline), #1406 (schema validator)
IMPORTANT: When this issue is complete, open a PR. Wait for merge to master before starting dependent issues. If you are Claude Code, do not proceed to dependent tasks in the same run.
Goal
Create the unified
journeys,journey_stops, andjourney_tagstables in the SQLite schema. Also retire the legacyconceptsandpeople_journeystables.What to build
1. Add new schema to
_tools/build_sqlite_loaders.pyFind the schema definition section (where tables like
chapters,sections,scholarsare defined) and add:2. Remove the legacy
conceptsandpeople_journeystable creationSearch
_tools/build_sqlite_loaders.pyfor:CREATE TABLE IF NOT EXISTS conceptsCREATE TABLE IF NOT EXISTS people_journeysDelete these CREATE statements entirely. Audit issue #1403 has the full inventory of references.
Evaluate
people_legacy_refs— if PersonDetailScreen still references it (check hooksusePersonJourney.ts), keep it for now; it's out of scope for this epic.3. Create
content/meta/journey-lenses.json[ { "id": "theological", "name": "Theological Development", "sort": 1 }, { "id": "biographical", "name": "Biographical Arc", "sort": 2 }, { "id": "narrative", "name": "Narrative Arc", "sort": 3 }, { "id": "questions", "name": "Theological Questions", "sort": 4 }, { "id": "comparative", "name": "Comparative Studies", "sort": 5 }, { "id": "cultural", "name": "Cultural Threads", "sort": 6 }, { "id": "reading_paths", "name": "Reading Paths & Intros", "sort": 7 }, { "id": "christological", "name": "Christological", "sort": 8 }, { "id": "liturgical", "name": "Liturgical & Devotional", "sort": 9 }, { "id": "hard_sayings", "name": "Hard Sayings & Apologetics", "sort": 10 }, { "id": "geographic", "name": "Geographic & Spatial", "sort": 11 }, { "id": "historical", "name": "Historical Context", "sort": 12 }, { "id": "application", "name": "Life Application", "sort": 13 }, { "id": "ane", "name": "Comparative Religion / ANE", "sort": 14 } ]4. Create
content/meta/journeys/directory structureCreate empty subdirectories:
content/meta/journeys/thematic/content/meta/journeys/concept/content/meta/journeys/person/Add a
.gitkeepin each so git tracks them.Acceptance criteria
journeys,journey_stops,journey_tagstables defined in_tools/build_sqlite_loaders.pyconceptsandpeople_journeysCREATE statements removedcontent/meta/journey-lenses.jsoncreated with all 14 lensescontent/meta/journeys/{thematic,concept,person}/directories existpython _tools/build_sqlite.pyruns without errors (will build empty journey tables; that's expected until Build pipeline: populate_journeys() in build_sqlite_loaders #1405 is done)Files to modify
_tools/build_sqlite_loaders.py— add new tables, remove retired onescontent/meta/journey-lenses.json— new filecontent/meta/journeys/{thematic,concept,person}/.gitkeep— new filesWorkflow