Skip to content

Content writer: save_journey() helper #1385

@CraigBuckmaster

Description

@CraigBuckmaster

Parent epic: #1379
Track: Code
Phase: 1 — Foundation

Dependency protocol

Blocked by: #2 (pipeline), #3 (validator)
Blocks: #5 (migration scripts), all content authoring (#11, #12, #13-22)

IMPORTANT: Do not begin until BOTH #2 and #3 PRs are merged. When complete, open a PR and wait for merge before any content work begins.

Goal

Add a save_journey() helper to _tools/content_writer.py so generator scripts (and chat authoring sessions) have a consistent, validated way to write journey JSON files.

What to build

1. Add save_journey() to _tools/content_writer.py

Pattern-match the existing save_chapter() function (which the team uses for all chapter content).

def save_journey(journey_type: str, data: dict) -> Path:
    """Validate and write a journey JSON file to the correct subdirectory.

    Args:
        journey_type: one of 'thematic', 'concept', 'person'
        data: the journey dict — must include 'id' and conform to journey schema

    Returns:
        Path to the written file.

    Raises:
        ValueError: if journey_type is invalid, data is malformed, or the ID
                    conflicts with an existing journey.
    """
    valid_types = {'thematic', 'concept', 'person'}
    if journey_type not in valid_types:
        raise ValueError(f"journey_type must be one of {valid_types}, got '{journey_type}'")

    if data.get('journey_type') != journey_type:
        raise ValueError(
            f"data['journey_type']='{data.get('journey_type')}' does not match "
            f"argument journey_type='{journey_type}'"
        )

    journey_id = data.get('id')
    if not journey_id or not re.match(r'^[a-z0-9][a-z0-9-]*$', journey_id):
        raise ValueError(f"Invalid journey id: '{journey_id}' — must be lowercase alphanumeric + hyphens")

    # Ensure required fields present (delegate full validation to schema_validator later)
    for required in ('title', 'description', 'stops'):
        if not data.get(required):
            raise ValueError(f"Required field missing or empty: '{required}'")

    if not isinstance(data['stops'], list) or len(data['stops']) == 0:
        raise ValueError("'stops' must be a non-empty list")

    # Validate stop ordering
    for i, stop in enumerate(data['stops'], start=1):
        if stop.get('stop_order') != i:
            raise ValueError(f"Stop at index {i-1} has stop_order={stop.get('stop_order')}, expected {i}")

    # Validate bridge_to_next pattern: non-null on all but last, null on last
    last_idx = len(data['stops']) - 1
    for i, stop in enumerate(data['stops']):
        bridge = stop.get('bridge_to_next')
        if i == last_idx:
            if bridge is not None and bridge != '':
                raise ValueError(f"Final stop (order {i+1}) must have bridge_to_next = null")
        else:
            if not bridge or not bridge.strip():
                raise ValueError(f"Stop at order {i+1} requires non-empty bridge_to_next")

    # Write the file
    out_dir = ROOT / 'content' / 'meta' / 'journeys' / journey_type
    out_dir.mkdir(parents=True, exist_ok=True)
    out_path = out_dir / f'{journey_id}.json'

    # Refuse to overwrite without explicit opt-in? — for now, overwrite to support iterative authoring
    with open(out_path, 'w', encoding='utf-8') as f:
        json.dump(data, f, indent=2, ensure_ascii=False)
        f.write('\n')

    return out_path

Imports required at top of file (add if missing):

import re
import json
from pathlib import Path

And ensure ROOT points to the repo root (match existing pattern in content_writer.py).

2. Ensure function is exportable

The function should be importable from _tools/ generator scripts:

from content_writer import save_journey

If content_writer.py has an explicit __all__ list, add 'save_journey'.

Acceptance criteria

  • save_journey() added to _tools/content_writer.py
  • Function validates: journey_type match, id format, required fields, stop ordering, bridge_to_next pattern
  • Function writes to content/meta/journeys/{type}/{id}.json
  • A minimal test script verifies the function:
    • Create a valid journey dict, call save_journey, verify file written
    • Call with invalid journey_type, verify ValueError raised
    • Call with non-sequential stop_order, verify ValueError raised
    • Call with bridge on final stop, verify ValueError raised
    • Delete test file after (no committed test fixtures)
  • python _tools/schema_validator.py still passes after save_journey writes a test file (round-trip check)
  • PR opened, wait for merge before Merge pull request #4 from CraigBuckmaster/master #5 and any content work

Files to modify

  • _tools/content_writer.py

Workflow

git checkout master && git pull  # ensure #2 and #3 merged
git checkout -b feature/1379-save-journey-helper
# add save_journey function
# run test script per acceptance criteria
git add -A && git commit -m "Content writer: save_journey() helper (#1379)"
git push  # PR, wait for merge

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions