#!/usr/bin/env python3
"""
analyze.py — discovery analysis: quantify a model's BEHAVIORAL SIGNATURE from local
Claude Code transcripts, side-by-side with a baseline model.

  python3 analyze.py                                   # claude-fable-5 vs auto-detected baseline
  python3 analyze.py --model claude-fable-5 --baseline claude-opus-4-8

What it measures (the only mineable layers):
  - narration: tool:text ratio, prose-emission %, self-opener %, words/msg
  - authoring: median/mean chars of .md docs the model Wrote
  - HONESTY PROBE: counts `thinking` blocks and their char length. They are stored
    0-char (encrypted) -> live reasoning is NOT mineable. Printed, not asserted.

Read-only. Copies nothing. See SKILL.md for how to read the numbers (which move, which don't).
"""
import argparse, json, os, re, statistics, sys
from collections import defaultdict

SELF = ("i'll", "let me", "i will", "let's", "i'm going to", "i can ", "i'd ", "i am going",
        "i'm going", "now i", "first, i", "i need to", "i should")
PROJ_ROOT = os.path.expanduser('~/.claude/projects')


def walk(d):
    for root, _, files in os.walk(d):
        for fn in files:
            if fn.endswith('.jsonl'):
                yield os.path.join(root, fn)


def blank(): return {
    'asst': 0, 'text': 0, 'tool': 0, 'prose': 0, 'selfopen': 0,
    'words': [], 'think_blocks': 0, 'think_chars': 0, 'doc_chars': [],
}


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('--model', default='claude-fable-5')
    ap.add_argument('--baseline', default=None, help='compare against (default: most frequent other claude-* model)')
    ap.add_argument('--root', default=PROJ_ROOT)
    args = ap.parse_args()
    root = os.path.expanduser(args.root)
    if not os.path.isdir(root):
        sys.exit(f'no transcripts at {root}')

    stats = defaultdict(blank)
    model_msgs = defaultdict(int)
    for f in walk(root):
        try:
            lines = open(f, errors='ignore').read().split('\n')
        except Exception:
            continue
        for ln in lines:
            if '"assistant"' not in ln or 'claude-' not in ln:
                continue
            try:
                o = json.loads(ln)
            except Exception:
                continue
            m = o.get('message') or {}
            if m.get('role') != 'assistant':
                continue
            model = m.get('model') or ''
            if not model.startswith('claude-'):
                continue
            model_msgs[model] += 1
            c = m.get('content')
            if not isinstance(c, list):
                continue
            s = stats[model]
            s['asst'] += 1
            txt = ''
            for b in c:
                if not isinstance(b, dict):
                    continue
                t = b.get('type')
                if t == 'text':
                    s['text'] += 1
                    txt += b.get('text', '')
                elif t == 'tool_use':
                    s['tool'] += 1
                    if b.get('name') == 'Write':
                        fp = (b.get('input') or {}).get('file_path', '')
                        if fp.endswith('.md'):
                            s['doc_chars'].append(len((b.get('input') or {}).get('content', '')))
                elif t == 'thinking':
                    s['think_blocks'] += 1
                    s['think_chars'] += len(b.get('thinking', '') or b.get('text', '') or '')
            if txt.strip():
                s['prose'] += 1
                s['words'].append(len(txt.split()))
                if txt.lower().lstrip().startswith(SELF):
                    s['selfopen'] += 1

    model = args.model
    if model not in stats:
        sys.exit(f'{model} not found on this machine. Ran: grep -rl {model} {root}\n'
                 f'models present: {sorted(model_msgs, key=model_msgs.get, reverse=True)[:6]}')
    baseline = args.baseline or next((mm for mm in sorted(model_msgs, key=model_msgs.get, reverse=True)
                                      if mm != model), None)

    def row(label, mdl):
        s = stats[mdl]
        ttr = s['tool'] / (s['text'] or 1)
        pe = 100 * s['prose'] / (s['asst'] or 1)
        so = 100 * s['selfopen'] / (s['prose'] or 1)
        mw = statistics.median(s['words']) if s['words'] else 0
        dc = statistics.median(s['doc_chars']) if s['doc_chars'] else 0
        print(f'  {label:<22} {mdl:<20} msgs={s["asst"]:<6} '
              f'tool:text={ttr:.2f}  prose={pe:.1f}%  self-open={so:.1f}%  '
              f'med-words={int(mw)}  med-doc={int(dc):,}c')
        return s

    print(f'\nBEHAVIORAL SIGNATURE — {model} vs baseline\n' + '=' * 78)
    sm = row('TARGET', model)
    if baseline:
        sb = row('BASELINE', baseline)

    print('\nHONESTY PROBE — is internal reasoning mineable?\n' + '-' * 78)
    print(f'  {model}: {sm["think_blocks"]} thinking blocks, {sm["think_chars"]} total chars')
    verdict = ('0 chars -> thinking is ENCRYPTED. Cognition is NOT mineable. '
               'Only behavior + authored artifacts are.') if sm['think_chars'] == 0 else \
              ('thinking text present -> reasoning partially mineable on THIS machine.')
    print(f'  => {verdict}')

    print('\nREAD THE NUMBERS (lessons baked in):\n' + '-' * 78)
    print('  - self-opener % + words/msg  = opener FORM. Transfers to other models via prompt. VALIDATE.')
    print('  - tool:text / prose %        = narration FREQUENCY. Weights-bound; a prompt moves it weakly.')
    print('  - med-doc chars              = authoring depth. Terseness scales docs down, does not gut them.')
    print('  - Any imitation claim needs matched-pair RED->GREEN, n>=3, report only non-overlapping spreads.')


if __name__ == '__main__':
    main()
