2026-05-01 21:39:00 +01:00
""" Validate Ghostlight lore grounding digest examples.
This focused validator checks the structural invariants needed before lore
digests can feed grounded projection fixtures .
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from typing import Any
ROOT = Path ( __file__ ) . resolve ( ) . parents [ 1 ]
2026-05-01 21:51:13 +01:00
DEFAULT_EXAMPLES = sorted ( ( ROOT / " examples " / " lore-grounding " ) . glob ( " *.json " ) )
2026-05-01 21:39:00 +01:00
class ValidationError ( Exception ) :
pass
def load_json ( path : Path ) - > Any :
with path . open ( " r " , encoding = " utf-8 " ) as handle :
return json . load ( handle )
def require ( condition : bool , message : str ) - > None :
if not condition :
raise ValidationError ( message )
def require_keys ( obj : dict [ str , Any ] , keys : list [ str ] , path : str ) - > None :
missing = [ key for key in keys if key not in obj ]
require ( not missing , f " { path } missing required keys: { ' , ' . join ( missing ) } " )
def require_nonempty_string ( value : Any , path : str ) - > None :
require ( isinstance ( value , str ) and value . strip ( ) , f " { path } must be a non-empty string " )
def require_string_list ( value : Any , path : str ) - > None :
require ( isinstance ( value , list ) , f " { path } must be an array " )
for index , item in enumerate ( value ) :
require_nonempty_string ( item , f " { path } [ { index } ] " )
def validate_sourced_text ( obj : Any , path : str ) - > None :
require ( isinstance ( obj , dict ) , f " { path } must be an object " )
require_keys ( obj , [ " text " , " source_refs " ] , path )
require_nonempty_string ( obj [ " text " ] , f " { path } .text " )
require_string_list ( obj [ " source_refs " ] , f " { path } .source_refs " )
def validate_sourced_text_list ( value : Any , path : str ) - > None :
require ( isinstance ( value , list ) , f " { path } must be an array " )
for index , item in enumerate ( value ) :
validate_sourced_text ( item , f " { path } [ { index } ] " )
def validate_digest ( document : dict [ str , Any ] , source : Path ) - > None :
require ( document . get ( " schema_version " ) == " ghostlight.lore_grounding_digest.v0 " , f " { source } has wrong schema_version " )
require_keys (
document ,
[
" digest_id " ,
" source_status " ,
" fixture_status " ,
" source_refs " ,
" historical_context " ,
" groups " ,
" cultural_pressure_fields " ,
" factional_misread_matrix " ,
" role_pressure_fields " ,
" speaker_lore_boundaries " ,
" author_only_boundaries " ,
" open_questions " ,
] ,
str ( source ) ,
)
require ( document [ " source_status " ] in { " template " , " draft " , " reviewed " , " superseded " } , f " { source } .source_status is invalid " )
require ( document [ " fixture_status " ] in { " historical_grounded " , " transition_grounded " , " procedural_branch " , " promoted_branch " } , f " { source } .fixture_status is invalid " )
source_ids : set [ str ] = set ( )
for index , ref in enumerate ( document [ " source_refs " ] ) :
path = f " { source } .source_refs[ { index } ] "
require_keys ( ref , [ " source_id " , " path " , " relevance " ] , path )
require_nonempty_string ( ref [ " source_id " ] , f " { path } .source_id " )
require_nonempty_string ( ref [ " path " ] , f " { path } .path " )
require_nonempty_string ( ref [ " relevance " ] , f " { path } .relevance " )
source_ids . add ( ref [ " source_id " ] )
require ( source_ids , f " { source } .source_refs must not be empty " )
context = document [ " historical_context " ]
require_keys ( context , [ " time_period " , " location " , " summary " , " public_facts " , " ambient_pressures " ] , f " { source } .historical_context " )
for key in [ " time_period " , " location " , " summary " ] :
require_nonempty_string ( context [ key ] , f " { source } .historical_context. { key } " )
validate_sourced_text_list ( context [ " public_facts " ] , f " { source } .historical_context.public_facts " )
validate_sourced_text_list ( context [ " ambient_pressures " ] , f " { source } .historical_context.ambient_pressures " )
group_ids : set [ str ] = set ( )
for index , group in enumerate ( document [ " groups " ] ) :
path = f " { source } .groups[ { index } ] "
require_keys (
group ,
[
" group_id " ,
" label " ,
" group_type " ,
" institutional_logic " ,
" material_incentives " ,
" internal_virtues " ,
" internal_shames " ,
" prestige_markers " ,
" taboo_triggers " ,
" emotional_display_norms " ,
" trust_defaults " ,
" speech_register_cues " ,
" source_refs " ,
] ,
path ,
)
require_nonempty_string ( group [ " group_id " ] , f " { path } .group_id " )
group_ids . add ( group [ " group_id " ] )
for key in [
" institutional_logic " ,
" material_incentives " ,
" internal_virtues " ,
" internal_shames " ,
" prestige_markers " ,
" taboo_triggers " ,
" emotional_display_norms " ,
" trust_defaults " ,
" speech_register_cues " ,
" source_refs " ,
] :
require_string_list ( group [ key ] , f " { path } . { key } " )
require ( group_ids , f " { source } .groups must not be empty " )
for index , field in enumerate ( document [ " cultural_pressure_fields " ] ) :
path = f " { source } .cultural_pressure_fields[ { index } ] "
require_keys ( field , [ " field_id " , " applies_to " , " pressure_type " , " summary " , " projection_use " , " source_refs " ] , path )
require_string_list ( field [ " applies_to " ] , f " { path } .applies_to " )
for group_id in field [ " applies_to " ] :
require ( group_id in group_ids , f " { path } .applies_to references unknown group: { group_id } " )
require_nonempty_string ( field [ " summary " ] , f " { path } .summary " )
require_nonempty_string ( field [ " projection_use " ] , f " { path } .projection_use " )
require_string_list ( field [ " source_refs " ] , f " { path } .source_refs " )
for index , misread in enumerate ( document [ " factional_misread_matrix " ] ) :
path = f " { source } .factional_misread_matrix[ { index } ] "
require_keys (
misread ,
[
" observer_group_id " ,
" target_group_id " ,
" observer_read " ,
" target_self_read " ,
" misread_patterns " ,
" likely_insults " ,
" projection_use " ,
" source_refs " ,
] ,
path ,
)
for key in [ " observer_group_id " , " target_group_id " ] :
require ( misread [ key ] in group_ids , f " { path } . { key } references unknown group: { misread [ key ] } " )
for key in [ " observer_read " , " target_self_read " , " misread_patterns " , " likely_insults " , " source_refs " ] :
require_string_list ( misread [ key ] , f " { path } . { key } " )
require_nonempty_string ( misread [ " projection_use " ] , f " { path } .projection_use " )
for index , role in enumerate ( document [ " role_pressure_fields " ] ) :
path = f " { source } .role_pressure_fields[ { index } ] "
require_keys ( role , [ " role_id " , " summary " , " obligations " , " available_masks " , " failure_modes " , " source_refs " ] , path )
require_nonempty_string ( role [ " role_id " ] , f " { path } .role_id " )
require_nonempty_string ( role [ " summary " ] , f " { path } .summary " )
for key in [ " obligations " , " available_masks " , " failure_modes " , " source_refs " ] :
require_string_list ( role [ key ] , f " { path } . { key } " )
for index , boundary in enumerate ( document [ " speaker_lore_boundaries " ] ) :
path = f " { source } .speaker_lore_boundaries[ { index } ] "
require_keys ( boundary , [ " speaker_id " , " known_context " , " beliefs " , " misreads " , " must_not_know " ] , path )
require_nonempty_string ( boundary [ " speaker_id " ] , f " { path } .speaker_id " )
for key in [ " known_context " , " beliefs " , " misreads " , " must_not_know " ] :
validate_sourced_text_list ( boundary [ key ] , f " { path } . { key } " )
validate_sourced_text_list ( document [ " author_only_boundaries " ] , f " { source } .author_only_boundaries " )
for index , question in enumerate ( document [ " open_questions " ] ) :
path = f " { source } .open_questions[ { index } ] "
require_keys ( question , [ " question " , " blocks_training " , " notes " ] , path )
require_nonempty_string ( question [ " question " ] , f " { path } .question " )
require ( isinstance ( question [ " blocks_training " ] , bool ) , f " { path } .blocks_training must be boolean " )
require_nonempty_string ( question [ " notes " ] , f " { path } .notes " )
def main ( ) - > int :
parser = argparse . ArgumentParser ( description = " Validate Ghostlight lore grounding digest examples. " )
parser . add_argument ( " paths " , nargs = " * " , type = Path , help = " Lore grounding digest JSON files to validate. " )
args = parser . parse_args ( )
paths = args . paths or DEFAULT_EXAMPLES
for path in paths :
validate_digest ( load_json ( path ) , path )
print ( f " ok: { path } " )
return 0
if __name__ == " __main__ " :
raise SystemExit ( main ( ) )