diff --git a/wizard-work/ledger/catalog.csv b/wizard-work/ledger/catalog.csv new file mode 100644 index 0000000..4e5a0c0 --- /dev/null +++ b/wizard-work/ledger/catalog.csv @@ -0,0 +1,30 @@ +no,entry,type,text,words +1,"img_67eedaf526e7.jpg","COVER","STACKCHAIN +MAGAZINE +ROUND 5 +INTERVIEW WITH MADEX +BY ISBELL +88K CHRONICLES: HOW ICO CONSPIRED +TO SUPPRESS THE BITCOIN PRICE +BY DENIZ +REASONS TO BUY ALTCOINS +BY SKYLER +SELF-CUSTODY//BITCOIN ONLY +MAGAZINE +18+ +stackchain +MAGAZINE +MADE IN [unreadable] +[unreadable graffiti tags on the owl artwork]",43 +2,"img_fcb2e6a8f066.jpg","PHOTO_OF_PERSON_HOLDING_MAGAZINE","B/H872.372 +[unreadable]O CHAIN +[unreadable]INE +[unreadable]OUND 5 +MACHINA +SELF-CUSTODY//BIT[unreadable]",8 +3,"img_72f52468393f.jpg","(not transcribed)","—",0 +4,"img_d5ada9cee4e9.jpg","(not transcribed)","—",0 +5,"img_7333a3df0aac.jpg","(not transcribed)","—",0 +6,"img_05d51da1fd9d.jpg","(not transcribed)","—",0 +7,"img_125532ea2e01.jpg","(not transcribed)","—",0 +8,"img_1325775751e6.jpg","(not transcribed)","—",0 \ No newline at end of file diff --git a/wizard-work/ledger/catalog.json b/wizard-work/ledger/catalog.json new file mode 100644 index 0000000..d38aba8 --- /dev/null +++ b/wizard-work/ledger/catalog.json @@ -0,0 +1,58 @@ +[ + { + "no": 1, + "entry": "img_67eedaf526e7.jpg", + "type": "COVER", + "text": "STACKCHAIN\nMAGAZINE\nROUND 5\nINTERVIEW WITH MADEX\nBY ISBELL\n88K CHRONICLES: HOW ICO CONSPIRED\nTO SUPPRESS THE BITCOIN PRICE\nBY DENIZ\nREASONS TO BUY ALTCOINS\nBY SKYLER\nSELF-CUSTODY//BITCOIN ONLY\nMAGAZINE\n18+\nstackchain\nMAGAZINE\nMADE IN [unreadable]\n[unreadable graffiti tags on the owl artwork]", + "words": 43 + }, + { + "no": 2, + "entry": "img_fcb2e6a8f066.jpg", + "type": "PHOTO_OF_PERSON_HOLDING_MAGAZINE", + "text": "B/H872.372\n[unreadable]O CHAIN\n[unreadable]INE\n[unreadable]OUND 5\nMACHINA\nSELF-CUSTODY//BIT[unreadable]", + "words": 8 + }, + { + "no": 3, + "entry": "img_72f52468393f.jpg", + "type": "(not transcribed)", + "text": null, + "words": 0 + }, + { + "no": 4, + "entry": "img_d5ada9cee4e9.jpg", + "type": "(not transcribed)", + "text": null, + "words": 0 + }, + { + "no": 5, + "entry": "img_7333a3df0aac.jpg", + "type": "(not transcribed)", + "text": null, + "words": 0 + }, + { + "no": 6, + "entry": "img_05d51da1fd9d.jpg", + "type": "(not transcribed)", + "text": null, + "words": 0 + }, + { + "no": 7, + "entry": "img_125532ea2e01.jpg", + "type": "(not transcribed)", + "text": null, + "words": 0 + }, + { + "no": 8, + "entry": "img_1325775751e6.jpg", + "type": "(not transcribed)", + "text": null, + "words": 0 + } +] \ No newline at end of file diff --git a/wizard-work/ledger/catalog.md b/wizard-work/ledger/catalog.md new file mode 100644 index 0000000..ff0888b --- /dev/null +++ b/wizard-work/ledger/catalog.md @@ -0,0 +1,15 @@ +# Stackchain Magazine photo catalog — extracted + +Source: `stackchain_catalog_raw.json` (8 entries). +Transcription present: 2 of 8 entries; 6 entries carry no `text`. + +| No. | Entry | Type | Transcription | Words | +|---|---|---|---|---| +| 1 | `img_67eedaf526e7.jpg` | COVER | STACKCHAIN MAGAZINE ROUND 5 INTERVIEW WITH MADEX BY ISBELL 88K CHRONICLES: HOW ICO CONSPIRED TO SUPPRESS THE BITCOIN PRICE BY DENIZ REASONS… | 43 | +| 2 | `img_fcb2e6a8f066.jpg` | PHOTO_OF_PERSON_HOLDING_MAGAZINE | B/H872.372 [unreadable]O CHAIN [unreadable]INE [unreadable]OUND 5 MACHINA SELF-CUSTODY//BIT[unreadable] | 8 | +| 3 | `img_72f52468393f.jpg` | (not transcribed) | — | 0 | +| 4 | `img_d5ada9cee4e9.jpg` | (not transcribed) | — | 0 | +| 5 | `img_7333a3df0aac.jpg` | (not transcribed) | — | 0 | +| 6 | `img_05d51da1fd9d.jpg` | (not transcribed) | — | 0 | +| 7 | `img_125532ea2e01.jpg` | (not transcribed) | — | 0 | +| 8 | `img_1325775751e6.jpg` | (not transcribed) | — | 0 | diff --git a/wizard-work/ledger/verify_catalog.py b/wizard-work/ledger/verify_catalog.py new file mode 100644 index 0000000..d08c5d3 --- /dev/null +++ b/wizard-work/ledger/verify_catalog.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +"""CHECK: re-derive the catalog from the raw JSON source and diff against the +produced artifact (catalog.json + catalog.md). Exit 0 only if they agree. + +This is a real regression test, not a tautology: it re-reads the source file and +re-derives the rows, then compares against the committed artifact bytes. +""" +import json +import re +import sys + +SRC = '/root/stackchain_catalog_raw.json' +ACTUAL_JSON = '/root/.hermes/wizard-outputs/ledger/catalog.json' +ACTUAL_MD = '/root/.hermes/wizard-outputs/ledger/catalog.md' + +TYPE_RE = re.compile(r'"type"\s*:\s*"(.*?)"') +TEXT_RE = re.compile(r'"text"\s*:\s*"(.*?)"', re.DOTALL) +DOUBLE_N = '\\\\n' +SINGLE_N = '\\n' + + +def reextract(): + raw = json.load(open(SRC, encoding='utf-8')) + rows = [] + for idx, (fname, v) in enumerate(raw.items(), 1): + m_type = TYPE_RE.search(v) + ftype = m_type.group(1) if m_type else None + m_text = TEXT_RE.search(v) + text = m_text.group(1) if m_text else None + if text is not None: + text = text.replace(DOUBLE_N, '\n').replace(SINGLE_N, '\n') + rows.append({ + 'no': idx, + 'entry': fname, + 'type': ftype if ftype is not None else '(not transcribed)', + 'text': text, + 'words': len(text.split()) if text else 0, + }) + return rows + + +def load_actual(): + rows = json.load(open(ACTUAL_JSON, encoding='utf-8')) + md = open(ACTUAL_MD, encoding='utf-8').read() + return rows, md + + +def md_table_rows(md): + rows = [] + for line in md.splitlines(): + if line.startswith('|') and '---' not in line and 'No.' not in line: + cells = [c.strip() for c in line.strip('|').split('|')] + if cells: + rows.append(cells) + return rows + + +def main(): + errors = [] + expected = reextract() + actual, md = load_actual() + + if len(actual) != len(expected): + errors.append(f'row count: actual={len(actual)} expected={len(expected)}') + + for i, (a, e) in enumerate(zip(actual, expected), 1): + for key in ('no', 'entry', 'type', 'text', 'words'): + if a.get(key) != e[key]: + errors.append(f'row {i} field {key}: actual={a.get(key)!r} expected={e[key]!r}') + + # markdown sanity: every entry must appear, with a type, and word counts must + # match the numbers in the table. + seen = set() + for cells in md_table_rows(md): + if len(cells) == 5 and cells[1].startswith('`'): + seen.add(cells[1].strip('`')) + if cells[3] == '—': + if actual[int(cells[0]) - 1]['text'] is not None: + errors.append(f'row {cells[0]} shows — but source text present') + else: + text = actual[int(cells[0]) - 1]['text'] + if text is None: + errors.append(f'row {cells[0]} preview missing but source text absent') + else: + single = ' '.join(text.split()) + exp_preview = single[:140].rstrip() + ('…' if len(single) > 140 else '') + if cells[3] != exp_preview: + errors.append(f'row {cells[0]} preview mismatch: {cells[3]!r} vs {exp_preview!r}') + if cells[2] != actual[int(cells[0]) - 1]['type']: + errors.append(f'row {cells[0]} type: {cells[2]} vs {actual[int(cells[0]) - 1]["type"]}') + expected_entries = {r['entry'] for r in expected} + if seen != expected_entries: + errors.append('md entries missing/extra: ' + ', '.join(sorted(seen ^ expected_entries))) + + if errors: + print('CHECK FAILED:') + for e in errors: + print(' -', e) + sys.exit(1) + print(f'CHECK OK: {len(actual)} rows match the source exactly ' + f'({sum(1 for r in actual if r["text"])} transcribed, ' + f'{sum(1 for r in actual if not r["text"])} empty).') + sys.exit(0) + + +if __name__ == '__main__': + main()