ledger: extract stackchain magazine photo catalog into a table
This commit is contained in:
1 parent
f44a285454
commit
c0dea95d77
4 files changed
+210
No files matched your search
@@ -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
|
||||
|
@@ -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
|
||||
}
|
||||
]
|
||||
@@ -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 |
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user