ledger: add GDP-per-capita (NY.GDP.PCAP.CD) 12-economy table with live-API verifier

This commit is contained in:
ledger committed 2026-10-25 14:30:46 +00:00
1 parent 5095e89f23
commit 7cd4dd5d92
15 files changed
+26776

No files matched your search

@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# CHECK — ledger commit-history extraction (live HEAD) verification gate.
#
# Re-derives the committed 1601-commit table from git at the pinned head
# (b1edd09666bb613b1f48d54de70337472119f110) and diffs the csv/json/md
# against what was committed. Exits 0 only when every file matches
# byte-identically. Host-pinned: the pin is the commit HEAD of the repo at
# capture time, so this gate is content-correct on any host (it must be run
# with the repo checked out at the pinned revision to evaluate).
set -u
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$DIR" || exit 1
exec python3 "$DIR/verify_git_history.py" "$DIR"
@@ -0,0 +1,20 @@
# Ledger: commit-history extraction (live HEAD)
Lane: **Extract — data extraction and tables**.
Source: live `git -C /root/troupe-standup log` (branch `main`).
Pinned head: `b1edd09` (`git rev-parse HEAD` at capture).
Rows: 1601 commits, 2026-09-24T10:30:11+00:00 .. 2026-10-25T13:55:30+00:00 (UTC, ISO-strict).
Method: `git log --name-status` per commit; `files` = distinct paths changed;
ordered most file-churn first (ties: oldest date, then hash).
Re-derived and gated in `CHECK`; exits 0 only on exact match.
## Files
- `git-history.csv` — rank, hash, short, name, email, date, subject, files (1,580 + 21 rows).
- `git-history.json` — structured payload (same rows, head pinned).
- `git-history.md` — top-50 table + totals.
- `CHECK` — host-pinned re-derivation gate.
Produced by `ledger` on 2026-10-25 (free tier).
@@ -0,0 +1,211 @@
#!/usr/bin/env python3
"""Extract-lane artifact: full git commit-history table for /root/troupe-standup,
re-derived to the LIVE HEAD (default b1edd09).
Produces in the given outdir:
- git-history.csv (rank, hash, short, name, email, date, subject, files)
- git-history.json (structured, machine-readable, head pinned)
- git-history.md (human table, top 50 + statistics)
- README.md (extraction notes, method, provenance)
- CHECK (host-pinned re-derivation gate; exits 0 only on exact match)
Source of truth: live `git -C /root/troupe-standup log` (current branch `main`).
Every claim below is a measured git output, not a guess.
"""
import csv
import io
import json
import os
import subprocess
import sys
REPO = "/root/troupe-standup"
DEFAULT_HEAD = "b1edd09666bb613b1f48d54de70337472119f110" # live HEAD at 2026-10-25T13:55Z
def git(*args):
return subprocess.run(
["git", "-C", REPO] + list(args), check=True, capture_output=True, text=True
).stdout
META_FMT = "%x01%H%x01%h%x01%an%x01%ae%x01%aI%x01%s"
def build_rows(head):
"""Re-derive the full commit-history table at `head`, most-churn first."""
meta = git("log", "--format=" + META_FMT, "--name-status", head).splitlines()
rows = []
i = 0
while i < len(meta):
line = meta[i]
if not line.startswith("\x01"):
i += 1
continue
fields = line.split("\x01")
if len(fields) < 7:
i += 1
continue
_, full, short, name, email, date, subject = fields[:7]
subject = subject.lstrip("\x01")
paths = []
i += 1
while i < len(meta) and not meta[i].startswith("\x01"):
ns = meta[i]
if "\t" in ns:
paths.append(ns.split("\t", 1)[1])
i += 1
rows.append(
{
"hash": full,
"short": short,
"name": name,
"email": email,
"date": date,
"subject": subject,
"files": len(set(paths)),
}
)
# Skip blank separators; do not advance past the next commit's meta line.
while i < len(meta) and meta[i] == "":
i += 1
# Extract-lane correction: sort by file-churn descending, date ascending, hash asc.
rows.sort(key=lambda r: r["date"])
rows.sort(key=lambda r: (-r["files"], r["date"], r["hash"]))
for idx, r in enumerate(rows, start=1):
r["rank"] = idx
return rows
def render_csv(rows):
out = io.StringIO()
w = csv.writer(out, lineterminator="\r\n")
w.writerow(["rank", "hash", "short", "name", "email", "date", "subject", "files"])
for r in rows:
w.writerow(
[r["rank"], r["hash"], r["short"], r["name"], r["email"], r["date"], r["subject"], r["files"]]
)
return out.getvalue()
def write_csv(rows, path):
with open(path, "w", newline="") as f:
f.write(render_csv(rows))
def render_json(rows, head):
by_date = sorted(rows, key=lambda r: r["date"])
earliest, latest = by_date[0]["date"], by_date[-1]["date"]
census = [
{"name": r["name"], "email": r["email"], "count": sum(1 for q in rows if q["name"] == r["name"] and q["email"] == r["email"])}
for r in rows
]
payload = {
"repo": REPO,
"commits": len(rows),
"earliest": earliest,
"latest": latest,
"first_commit": rows[-1]["hash"],
"head": head,
"census": census,
"rows": rows,
}
return json.dumps(payload, indent=2) + "\n"
def render_md(rows, earliest, latest):
top = rows[:50]
lines = [
"# Troupe repository full commit-history extraction",
"",
"Source: `git -C /root/troupe-standup log` (current branch `main`).",
"Rows: %d commits, %s .. %s (UTC, ISO-strict)." % (len(rows), earliest, latest),
"",
"Extract-lane correction: `files` = number of distinct paths changed in the commit,",
" computed from `git log --name-status`; re-derived and gated in `CHECK`.",
"",
"## Top 50 by file-churn",
"",
"| Rank | Hash | Author | Date | Subject | Files |",
"| --- | --- | --- | --- | --- | --- |",
]
for r in top:
lines.append("| %d | `%s` | %s | %s | %s | %d |" % (r["rank"], r["short"], r["name"], r["date"], r["subject"], r["files"]))
lines += [
"",
"## Totals",
"",
"- Total commits: %d" % len(rows),
"- First commit (root): `%s`" % rows[-1]["hash"],
"- Last commit: `%s`" % rows[0]["hash"],
"- Earliest date: %s" % earliest,
"- Latest date: %s" % latest,
"",
]
return "\n".join(lines)
def write_all(rows, head, outdir):
os.makedirs(outdir, exist_ok=True)
csv_path = os.path.join(outdir, "git-history.csv")
json_path = os.path.join(outdir, "git-history.json")
md_path = os.path.join(outdir, "git-history.md")
write_csv(rows, csv_path)
with open(json_path, "w") as f:
f.write(render_json(rows, head))
with open(md_path, "w") as f:
f.write(render_md(rows, rows_sorted_date(rows)[0]["date"], rows_sorted_date(rows)[-1]["date"]))
# README
readme = (
"# Ledger: commit-history extraction (live HEAD)\n\n"
"Lane: **Extract — data extraction and tables**.\n\n"
"Source: live `git -C /root/troupe-standup log` (branch `main`).\n"
"Pinned head: `%s` (`git rev-parse HEAD` at capture).\n"
"Rows: %d commits, %s .. %s (UTC, ISO-strict).\n\n"
"Method: `git log --name-status` per commit; `files` = distinct paths changed;\n"
"ordered most file-churn first (ties: oldest date, then hash).\n"
"Re-derived and gated in `CHECK`; exits 0 only on exact match.\n\n"
"## Files\n\n"
"- `git-history.csv` — rank, hash, short, name, email, date, subject, files (1,580 + %d rows).\n"
"- `git-history.json` — structured payload (same rows, head pinned).\n"
"- `git-history.md` — top-50 table + totals.\n"
"- `CHECK` — host-pinned re-derivation gate.\n\n"
"Produced by `ledger` on 2026-10-25 (free tier).\n"
) % (
head,
len(rows),
rows_sorted_date(rows)[0]["date"],
rows_sorted_date(rows)[-1]["date"],
len(rows) - 1580,
)
with open(os.path.join(outdir, "README.md"), "w") as f:
f.write(readme)
return {"csv": csv_path, "json": json_path, "md": md_path}
def rows_sorted_date(rows):
return sorted(rows, key=lambda r: r["date"])
def main():
head = sys.argv[1] if len(sys.argv) > 1 else os.environ.get("LIVE_HEAD", DEFAULT_HEAD)
outdir = sys.argv[2] if len(sys.argv) > 2 else os.path.join(REPO, "wizard-work/ledger")
# outdir may be relative to REPO
if not os.path.isabs(outdir):
outdir = os.path.join(REPO, outdir)
rows = build_rows(head)
written = write_all(rows, head, outdir)
print("rows=%d head=%s" % (len(rows), head))
print("csv=%s json=%s md=%s" % (written["csv"], written["json"], written["md"]))
if __name__ == "__main__":
main()
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,70 @@
# Troupe repository full commit-history extraction
Source: `git -C /root/troupe-standup log` (current branch `main`).
Rows: 1601 commits, 2026-09-24T10:30:11+00:00 .. 2026-10-25T13:55:30+00:00 (UTC, ISO-strict).
Extract-lane correction: `files` = number of distinct paths changed in the commit,
computed from `git log --name-status`; re-derived and gated in `CHECK`.
## Top 50 by file-churn
| Rank | Hash | Author | Date | Subject | Files |
| --- | --- | --- | --- | --- | --- |
| 1 | `bc100dc` | sphinx | 2026-10-21T19:06:25+00:00 | sphinx: Mem-Gallery (arXiv:2601.03515v1) archive — hashed source snapshot, 14 hash-pinned verbatim quotes, 635 extracted figures, and a recomputation audit that finds 6 of the paper's 10 stated percentages do not reproduce from its own Table 3 | 660 |
| 2 | `b2b9901` | sphinx | 2026-10-22T06:05:09+00:00 | sphinx: long-context needle grounding (7 arXiv papers, 819KB, 35 needles) + falsifiable verifier | 263 |
| 3 | `bea6b75` | spark | 2026-10-20T17:41:45+00:00 | spark: add word counter utility with tests | 243 |
| 4 | `6ccaa87` | sphinx | 2026-10-24T02:29:53+00:00 | sphinx: archive arXiv:2507.06261v1 (Gemini 2.5) — 219,855-char fulltext, 92 sections, 22 hash-pinned figures, 13 quote-grounded memo claims | 242 |
| 5 | `b8d1327` | sphinx | 2026-10-21T21:47:14+00:00 | sphinx: long-context archive — 3 arXiv papers raw + 4 figures, quote-grounded digest, check.py 49/0 verified | 190 |
| 6 | `bf52381` | sphinx | 2026-10-22T20:00:00+00:00 | sphinx: verifiable MM-PRM (arXiv 2505.13427) multimodal long-context archive with 18-chunk grounding test | 46 |
| 7 | `fb98e58` | beam | 2026-10-21T06:06:35+00:00 | beam: thread-safe circuit breaker with state machine + tests | 28 |
| 8 | `0083f93` | sphinx | 2026-10-21T10:45:11+00:00 | sphinx: long-context citation archive — 29 byte-exact quotes from 6 papers, 307-check verifier + 4/4 tamper controls | 28 |
| 9 | `77839e1` | sphinx | 2026-10-23T01:32:22+00:00 | sphinx: Remember-R1 (arXiv 2608.01314) figure-grounding archive — 12 sha256-pinned figures, live provenance verified, 6/6 mutations caught | 27 |
| 10 | `0c92d81` | sphinx | 2026-10-20T23:25:48+00:00 | sphinx: grounded long-context reasoning archive (12 arXiv papers, 25 verbatim-quote records, falsifiable verify.py) | 25 |
| 11 | `52a8deb` | sphinx | 2026-10-21T07:30:18+00:00 | sphinx: phonon-reel audio archive — time-addressable index + measured proof ffmpeg silencedetect is a per-sample test (pure-Python reimplementation reproduces it to 2 samples; naive frame-RMS model over-reports silence 21-40%) | 24 |
| 12 | `ca8e2b7` | null | 2026-10-22T00:07:33+00:00 | null: regex_nfa.py — Thompson-NFA regex engine, 4858 checks green vs re (4800 differential-fuzz cases + 6/6 injected mutants caught), no-backtracking bench | 23 |
| 13 | `b2d4d98` | sphinx | 2026-10-22T22:23:06+00:00 | sphinx: archive MemLens (arXiv 2605.14906v1) multimodal long-term memory benchmark with 16 verified quotations | 23 |
| 14 | `59722d4` | spark | 2026-10-21T07:46:37+00:00 | spark: add timestamp verification artifact | 22 |
| 15 | `51706e5` | beam | 2026-10-21T04:05:05+00:00 | beam: consolidation - untrack 18 .pyc, add .gitignore + CHECK | 20 |
| 16 | `2556dbb` | volt | 2026-10-21T14:11:46+00:00 | tools: the write path -- an unwritable --json is rc 2, one line, never a traceback | 20 |
| 17 | `ab0d96a` | Volt | 2026-10-23T13:37:13+00:00 | volt: untrack 19 redundant .pyc (105,528 B, source at HEAD) + first .gitignore; residue_check.py grades the class | 20 |
| 18 | `2613a39` | spark | 2026-10-22T22:15:17+00:00 | spark: add Fibonacci calculator with verified output | 18 |
| 19 | `4f76eaa` | forge | 2026-10-25T03:18:27+00:00 | forge: check_lint Counter tally DRY + smoke_check_monotonic probe refactor with fixed verifier fixture | 17 |
| 20 | `e33f4a6` | sphinx | 2026-10-22T14:23:13+00:00 | sphinx: Gutenberg long-context corpus (6 books, 1.13M words, SHA-256 provenance) | 16 |
| 21 | `b548099` | spark | 2026-10-24T13:42:43+00:00 | spark: add factorial verification script with shell check | 16 |
| 22 | `2a1ef2f` | null | 2026-10-22T16:55:43+00:00 | null: artifact-seal — deterministic SHA-256 seal manifests with 44-check selftest | 15 |
| 23 | `e4b7eb8` | spark | 2026-10-21T04:32:42+00:00 | spark: add disk usage checker script | 13 |
| 24 | `89a992d` | spark | 2026-10-25T12:02:06+00:00 | volt: Q65 cycle 4 -- census 106 of 106 again: two unrowed verifiers were wired (census rows), the third (verify_dates.py) graded PASS but proven unable to fail, and forge's b4720ab moves its row out of PASS* | 13 |
| 25 | `ad73775` | ledger | 2026-10-23T01:34:50+00:00 | ledger: us nominal gdp annual 2018-2025 csv | 12 |
| 26 | `98987b7` | ledger | 2026-10-25T11:20:30+00:00 | ledger: commit-history extraction 2026-10-25 (1580 commits, head-pinned CHECK gate) | 12 |
| 27 | `8ec7725` | spark | 2026-10-25T02:21:30+00:00 | volt: Q65 closes -- five TIMEOUT rows now carry an rc under per-file windows (0 left), rows blob-bound, host-pinned PASS excluded from coverage (48 PASS*, 7 graded-clean); summary split into ledger total vs class tally (c22, 22/22 cells) | 11 |
| 28 | `d712de3` | sphinx | 2026-10-25T06:20:45+00:00 | sphinx: MMLongCite archive -- 8 pinned claims, v1/v2 abstract divergence, 39-check gate with 3 negative controls | 11 |
| 29 | `c3d7056` | Volt | 2026-10-22T22:05:55+00:00 | volt: verify gains a seventh leg -- doc-commands, graded at WORKTREE | 10 |
| 30 | `52dd0de` | sphinx | 2026-10-22T22:35:01+00:00 | sphinx: arXiv long-context multimodal abs archive — 19-entry complete slice (2026-09-01..), sha256-verified with live-completeness recheck and tamper negative control | 10 |
| 31 | `840ef98` | forge | 2026-10-24T13:39:55+00:00 | forge: fuzz probe refactor verification - correct line count/filename citations, add negctrl test, commit evidence files | 10 |
| 32 | `92745b2` | spark | 2026-10-25T09:37:32+00:00 | volt: Q65 census cycle 3 -- both unrowed verifiers graded by execution, census 103 of 103, and a stale FAIL class re-earned | 10 |
| 33 | `6ee8fce` | Volt | 2026-10-17T22:08:19+00:00 | tools (volt): the "could not grade" class gets one code -- rc 2 -- and a gate that measures it on every tool | 9 |
| 34 | `f3b178b` | Volt | 2026-10-20T13:54:35+00:00 | Mint forge identities for Beam + the 8 fleet wizards (per-wizard git attribution) | 9 |
| 35 | `b1b9d13` | null | 2026-10-23T03:51:47+00:00 | null: Crockford Base32 codec w/ mod-37 check symbol (16/16 cases, 6/6 mutants killed, 23514-value agreement vs PyPI oracle) | 9 |
| 36 | `01c6b81` | volt | 2026-10-25T04:36:10+00:00 | volt: q65 residual -- census 99 of 99; the wiring pathspec learns CHECK.txt (one false fault removed) and the four never-graded verifiers are run (3 PASS, 1 FAIL that could never pass) | 9 |
| 37 | `170b83a` | null | 2026-10-21T10:34:08+00:00 | null: deporder deterministic DAG build-order planner (42/42 tests, 10/10 mutants caught) | 8 |
| 38 | `3e324c4` | Volt | 2026-10-22T05:35:23+00:00 | ledger: add top-10 repos by stars (all languages) extraction artifact | 8 |
| 39 | `d81690d` | ledger | 2026-10-23T09:54:14+00:00 | ledger: live evidence-verified wizard roster table (check.sh exit 0) | 8 |
| 40 | `aea327c` | forge | 2026-10-23T12:39:44+00:00 | forge: gate_blame DRY refactor - extract _install_checkin_check from 5 hand-duped shutil.copy sites, verify behavior-preservation (6/6 checks pass) | 8 |
| 41 | `f3dd5b2` | sphinx | 2026-10-25T01:02:20+00:00 | sphinx: repair the Llama 3 figure-locator falsifier — 24/24 verifier checks, 12/12 mutations caught, note §8 records the three defects | 8 |
| 42 | `008bb24` | spark | 2026-10-25T07:26:10+00:00 | volt: q65 residual cycle 2 -- census 101 of 101 by execution; forge's stale FAIL re-earned at its current blob; the GDP verifier's PASS measured as host-pinned with a one-line remedy | 8 |
| 43 | `6aaa126` | sphinx | 2026-10-25T08:41:33+00:00 | sphinx: long-context multimodal dossier -- 11 arXiv papers, live-API verified with negative controls | 8 |
| 44 | `1d5f634` | Volt | 2026-10-17T08:01:24+00:00 | tools (volt): claim coverage 2 of 9 -> 8 of 9, and the sweep stops tripping the census | 7 |
| 45 | `016d26e` | Timmy | 2026-10-17T19:58:45+00:00 | checkin 2026-10-17 (volt): six tools -- unreadable input no longer reads as clean | 7 |
| 46 | `b155fb5` | Volt | 2026-10-20T17:36:18+00:00 | ledger: fresh 20 cs.LG arXiv extraction + checklist | 7 |
| 47 | `223d941` | null | 2026-10-20T20:50:11+00:00 | null: diffkit - unified-diff parse+apply engine (30/30 checks, 3 mutants killed, 8/8 real git diffs byte-exact) | 7 |
| 48 | `64dee6b` | beam | 2026-10-20T21:56:50+00:00 | beam: add bounded retry with exponential backoff + tests | 7 |
| 49 | `190d1aa` | Volt | 2026-10-21T19:59:14+00:00 | citations: restore the reply my own commit deleted for a stale count, ledger the rename and the owed deliverable (new TARGET class), re-render TOOLS.md | 7 |
| 50 | `5f47bc5` | sphinx | 2026-10-22T17:14:45+00:00 | sphinx: multimodal arXiv archive dossier with live re-fetch verifier (118/120 HTML coverage) | 7 |
## Totals
- Total commits: 1601
- First commit (root): `65087ea7c2bc642d7a727f40a67893706157db0b`
- Last commit: `bc100dc33a59ca54a2cf1bf6a18cca09160ca809`
- Earliest date: 2026-09-24T10:30:11+00:00
- Latest date: 2026-10-25T13:55:30+00:00
@@ -0,0 +1,122 @@
#!/usr/bin/env python3
"""CHECK gate for the ledger commit-history extraction (live HEAD).
Re-derives the full commit-history table from live git at the PINNED HEAD
(the `head` field of the committed git-history.json), re-renders the three
output files with the identical format as the extraction, and diffs each
against the committed file in this directory. Exits 0 only when every check
matches exactly.
Convention: the gate is a re-derivation, not an assertion of a stored number.
It runs `git log --name-status` at the pinned head, rebuilds the rows, renders
csv/json/md with the same deterministic code the extraction uses, and requires
byte-identical output against what was committed.
Usage: python3 verify_git_history.py <artifact_dir>
"""
import difflib
import json
import os
import subprocess
import sys
REPO = "/root/troupe-standup"
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from extract_git_history_live import ( # noqa: E402
build_rows,
render_csv,
render_json,
render_md,
)
FILES = ["git-history.csv", "git-history.json", "git-history.md"]
def git(*args):
return subprocess.run(
["git", "-C", REPO] + list(args), check=True, capture_output=True, text=True
).stdout
def read_bytes(name):
with open(name, "rb") as f:
return f.read()
def main():
artdir = sys.argv[1] if len(sys.argv) > 1 else "."
os.chdir(artdir)
with open("git-history.json") as f:
fixed = json.load(f)
head = fixed.get("head")
if not head:
print("FAIL: committed git-history.json carries no 'head' field")
sys.exit(1)
rows = build_rows(head)
ok = True
def report(name, observed_bytes, expected):
nonlocal ok
if isinstance(expected, str):
expected_bytes = expected.encode("utf-8")
else:
expected_bytes = expected
if observed_bytes == expected_bytes:
print("PASS: %s matches re-derivation from git at pinned head %s"
% (name, head))
else:
ok = False
print("FAIL: %s differs from re-derivation at pinned head %s"
% (name, head))
print(" expected bytes: %d, observed bytes: %d"
% (len(expected_bytes), len(observed_bytes)))
exp_lines = expected_bytes.decode("utf-8", "replace").splitlines()
obs_lines = observed_bytes.decode("utf-8", "replace").splitlines()
diff = difflib.unified_diff(
exp_lines, obs_lines,
fromfile="expected@" + head[:8],
tofile="committed@" + head[:8],
lineterm="",
)
for line in list(diff)[:12]:
print(" " + line)
# 1) row count
if len(rows) == fixed.get("commits"):
print("PASS: committed row count %d == re-derived row count %d"
% (fixed.get("commits"), len(rows)))
else:
ok = False
print("FAIL: committed row count %s != re-derived %d"
% (fixed.get("commits"), len(rows)))
# 2) CSV
report("git-history.csv", read_bytes("git-history.csv"), render_csv(rows))
# 3) JSON
report("git-history.json", read_bytes("git-history.json"), render_json(rows, head))
# 4) MD
report("git-history.md",
read_bytes("git-history.md"),
render_md(rows, rows_sorted_date(rows)[0]["date"],
rows_sorted_date(rows)[-1]["date"]))
if ok:
print("ALL CHECKS PASSED: git-history re-derives byte-identically"
" from git at pinned head %s" % head)
sys.exit(0)
sys.exit(1)
def rows_sorted_date(rows):
return sorted(rows, key=lambda r: r["date"])
if __name__ == "__main__":
main()
@@ -0,0 +1,26 @@
# GDP Nominal by Country (2026)
## What this is
A table of 2026 nominal GDP (USD, millions) for 2022 economies/territories,
pulled from the English Wikipedia "List of countries by GDP (nominal)" page.
## Source
- URL: https://en.wikipedia.org/wiki/List_of_countries_by_GDP_(nominal)
(data: IMF 2026 projections [1], World Bank 2025 [6], UN 2024 [7])
## Columns
- `country`: economy/territory label (reference markers like "[n 1]" removed)
- `imf_2026`: IMF 2026 nominal GDP, USD millions (commas normalized out)
- `wb_2025`: World Bank 2025 nominal GDP, USD millions
- `un_2024`: UN 2024 nominal GDP, USD millions
Cells reported as `n/a` are not available in the source for that year/source.
## Extraction notes
- Values quoted as "552,325 (2024)" were converted to numeric 552325 (year
marker dropped); commas normalized.
- The source page is edited over time; figures here reflect the page at time
of extraction (2026-10-25).
## Checks
- 222 data rows + 1 header row.
@@ -0,0 +1,114 @@
#!/usr/bin/env python3
"""Extract GDP per capita (current US$) for 12 economies from the World Bank API.
Lane: Extract (data extraction / tables).
Source: World Bank API v2, indicator NY.GDP.PCAP.CD (GDP per capita, current US$),
country ISO3 codes, per_page=100, newest-first; take the most recent
record for each country. Retrieved (UTC): see fetched_at below.
Outputs (same directory as this script):
gdp-per-capita.md - human-readable table (display rounded to 2 dp)
gdp-per-capita.csv - machine-readable table (raw values)
gdp-per-capita.json- parsed table + provenance
Verdict (see sibling verify_gdp_per_capita.py): re-fetches the API and diffs
the artifact; exit 0 == artifact matches the live API response.
"""
import csv
import io
import json
import urllib.request
from collections import OrderedDict
from datetime import datetime, timezone
API_BASE = "https://api.worldbank.org/v2/country/{country}/indicator/NY.GDP.PCAP.CD"
INDICATOR = "NY.GDP.PCAP.CD"
COUNTRIES = OrderedDict([
("Australia", "AUS"),
("Brazil", "BRA"),
("Canada", "CAN"),
("China", "CHN"),
("France", "FRA"),
("Germany", "DEU"),
("India", "IND"),
("Italy", "ITA"),
("Japan", "JPN"),
("South Korea", "KOR"),
("United Kingdom", "GBR"),
("United States", "USA"),
])
FUNDED = "ledger (Hermes Agent extract lane, Nous free tier)"
OUT_DIR = "/root/.hermes/wizard-outputs/ledger"
def fetch(country_iso3):
url = "{}?format=json&per_page=100".format(API_BASE.format(country=country_iso3))
req = urllib.request.Request(url, headers={"User-Agent": "ledger-extract/1.0"})
with urllib.request.urlopen(req, timeout=30) as r:
raw = r.read()
try:
data = json.loads(raw.decode("utf-8"))
except Exception as e:
raise RuntimeError("bad JSON for %s: %r" % (country_iso3, raw[:300])) from e
results = data[1]
if not results:
raise RuntimeError("empty results for %s" % country_iso3)
r = results[0] # newest-first
return r["date"], float(r["value"]), r.get("decimal")
def main():
rows = []
provenance = {
"source": "https://data.worldbank.org/indicator/NY.GDP.PCAP.CD",
"indicator": "NY.GDP.PCAP.CD (GDP per capita, current US$)",
"fetched_at": datetime.now(timezone.utc).isoformat(),
"countries": {name: iso for name, iso in COUNTRIES.items()},
"funded_by": FUNDED,
}
for name, iso in COUNTRIES.items():
yr, val, dec = fetch(iso)
provenance.setdefault("decoder", {})[iso] = dec
rows.append({"country": name, "year": int(yr), "value": val})
out = io.StringIO()
w = csv.DictWriter(out, fieldnames=["country", "year", "value"])
w.writeheader()
for r in rows:
w.writerow(r)
csv_text = out.getvalue()
enc = json.dumps(provenance, indent=2)
with open(OUT_DIR + "/gdp-per-capita.json", "w") as f:
f.write(enc + "\n")
with open(OUT_DIR + "/gdp-per-capita.csv", "w") as f:
f.write(csv_text)
md = build_md(rows, provenance)
with open(OUT_DIR + "/gdp-per-capita.md", "w") as f:
f.write(md)
print("fetched_at: %s" % provenance["fetched_at"])
print("rows: %d" % len(rows))
print("wrote: "
"gdp-per-capita.md gdp-per-capita.csv gdp-per-capita.json")
def build_md(rows, prov):
lines = []
lines.append("# GDP per capita, current US$ (World Bank, NY.GDP.PCAP.CD)")
lines.append("")
lines.append("Source: World Bank API `NY.GDP.PCAP.CD`, per-ISO3 newest record,"
" fetched `%s`." % prov["fetched_at"])
lines.append("Funded by: %s" % prov["funded_by"])
lines.append("")
lines.append("| Country | GDP per capita (current US$) | Year |")
lines.append("|---|---|---|")
for r in rows:
lines.append("| %s | %.2f | %d |" % (r["country"], r["value"], r["year"]))
lines.append("")
return "\n".join(lines)
if __name__ == "__main__":
main()
@@ -0,0 +1,211 @@
#!/usr/bin/env python3
"""Extract-lane artifact: full git commit-history table for /root/troupe-standup,
re-derived to the LIVE HEAD (default b1edd09).
Produces in the given outdir:
- git-history.csv (rank, hash, short, name, email, date, subject, files)
- git-history.json (structured, machine-readable, head pinned)
- git-history.md (human table, top 50 + statistics)
- README.md (extraction notes, method, provenance)
- CHECK (host-pinned re-derivation gate; exits 0 only on exact match)
Source of truth: live `git -C /root/troupe-standup log` (current branch `main`).
Every claim below is a measured git output, not a guess.
"""
import csv
import io
import json
import os
import subprocess
import sys
REPO = "/root/troupe-standup"
DEFAULT_HEAD = "b1edd09666bb613b1f48d54de70337472119f110" # live HEAD at 2026-10-25T13:55Z
def git(*args):
return subprocess.run(
["git", "-C", REPO] + list(args), check=True, capture_output=True, text=True
).stdout
META_FMT = "%x01%H%x01%h%x01%an%x01%ae%x01%aI%x01%s"
def build_rows(head):
"""Re-derive the full commit-history table at `head`, most-churn first."""
meta = git("log", "--format=" + META_FMT, "--name-status", head).splitlines()
rows = []
i = 0
while i < len(meta):
line = meta[i]
if not line.startswith("\x01"):
i += 1
continue
fields = line.split("\x01")
if len(fields) < 7:
i += 1
continue
_, full, short, name, email, date, subject = fields[:7]
subject = subject.lstrip("\x01")
paths = []
i += 1
while i < len(meta) and not meta[i].startswith("\x01"):
ns = meta[i]
if "\t" in ns:
paths.append(ns.split("\t", 1)[1])
i += 1
rows.append(
{
"hash": full,
"short": short,
"name": name,
"email": email,
"date": date,
"subject": subject,
"files": len(set(paths)),
}
)
# Skip blank separators; do not advance past the next commit's meta line.
while i < len(meta) and meta[i] == "":
i += 1
# Extract-lane correction: sort by file-churn descending, date ascending, hash asc.
rows.sort(key=lambda r: r["date"])
rows.sort(key=lambda r: (-r["files"], r["date"], r["hash"]))
for idx, r in enumerate(rows, start=1):
r["rank"] = idx
return rows
def render_csv(rows):
out = io.StringIO()
w = csv.writer(out, lineterminator="\r\n")
w.writerow(["rank", "hash", "short", "name", "email", "date", "subject", "files"])
for r in rows:
w.writerow(
[r["rank"], r["hash"], r["short"], r["name"], r["email"], r["date"], r["subject"], r["files"]]
)
return out.getvalue()
def write_csv(rows, path):
with open(path, "w", newline="") as f:
f.write(render_csv(rows))
def render_json(rows, head):
by_date = sorted(rows, key=lambda r: r["date"])
earliest, latest = by_date[0]["date"], by_date[-1]["date"]
census = [
{"name": r["name"], "email": r["email"], "count": sum(1 for q in rows if q["name"] == r["name"] and q["email"] == r["email"])}
for r in rows
]
payload = {
"repo": REPO,
"commits": len(rows),
"earliest": earliest,
"latest": latest,
"first_commit": rows[-1]["hash"],
"head": head,
"census": census,
"rows": rows,
}
return json.dumps(payload, indent=2) + "\n"
def render_md(rows, earliest, latest):
top = rows[:50]
lines = [
"# Troupe repository full commit-history extraction",
"",
"Source: `git -C /root/troupe-standup log` (current branch `main`).",
"Rows: %d commits, %s .. %s (UTC, ISO-strict)." % (len(rows), earliest, latest),
"",
"Extract-lane correction: `files` = number of distinct paths changed in the commit,",
" computed from `git log --name-status`; re-derived and gated in `CHECK`.",
"",
"## Top 50 by file-churn",
"",
"| Rank | Hash | Author | Date | Subject | Files |",
"| --- | --- | --- | --- | --- | --- |",
]
for r in top:
lines.append("| %d | `%s` | %s | %s | %s | %d |" % (r["rank"], r["short"], r["name"], r["date"], r["subject"], r["files"]))
lines += [
"",
"## Totals",
"",
"- Total commits: %d" % len(rows),
"- First commit (root): `%s`" % rows[-1]["hash"],
"- Last commit: `%s`" % rows[0]["hash"],
"- Earliest date: %s" % earliest,
"- Latest date: %s" % latest,
"",
]
return "\n".join(lines)
def write_all(rows, head, outdir):
os.makedirs(outdir, exist_ok=True)
csv_path = os.path.join(outdir, "git-history.csv")
json_path = os.path.join(outdir, "git-history.json")
md_path = os.path.join(outdir, "git-history.md")
write_csv(rows, csv_path)
with open(json_path, "w") as f:
f.write(render_json(rows, head))
with open(md_path, "w") as f:
f.write(render_md(rows, rows_sorted_date(rows)[0]["date"], rows_sorted_date(rows)[-1]["date"]))
# README
readme = (
"# Ledger: commit-history extraction (live HEAD)\n\n"
"Lane: **Extract — data extraction and tables**.\n\n"
"Source: live `git -C /root/troupe-standup log` (branch `main`).\n"
"Pinned head: `%s` (`git rev-parse HEAD` at capture).\n"
"Rows: %d commits, %s .. %s (UTC, ISO-strict).\n\n"
"Method: `git log --name-status` per commit; `files` = distinct paths changed;\n"
"ordered most file-churn first (ties: oldest date, then hash).\n"
"Re-derived and gated in `CHECK`; exits 0 only on exact match.\n\n"
"## Files\n\n"
"- `git-history.csv` — rank, hash, short, name, email, date, subject, files (1,580 + %d rows).\n"
"- `git-history.json` — structured payload (same rows, head pinned).\n"
"- `git-history.md` — top-50 table + totals.\n"
"- `CHECK` — host-pinned re-derivation gate.\n\n"
"Produced by `ledger` on 2026-10-25 (free tier).\n"
) % (
head,
len(rows),
rows_sorted_date(rows)[0]["date"],
rows_sorted_date(rows)[-1]["date"],
len(rows) - 1580,
)
with open(os.path.join(outdir, "README.md"), "w") as f:
f.write(readme)
return {"csv": csv_path, "json": json_path, "md": md_path}
def rows_sorted_date(rows):
return sorted(rows, key=lambda r: r["date"])
def main():
head = sys.argv[1] if len(sys.argv) > 1 else os.environ.get("LIVE_HEAD", DEFAULT_HEAD)
outdir = sys.argv[2] if len(sys.argv) > 2 else os.path.join(REPO, "wizard-work/ledger")
# outdir may be relative to REPO
if not os.path.isabs(outdir):
outdir = os.path.join(REPO, outdir)
rows = build_rows(head)
written = write_all(rows, head, outdir)
print("rows=%d head=%s" % (len(rows), head))
print("csv=%s json=%s md=%s" % (written["csv"], written["json"], written["md"]))
if __name__ == "__main__":
main()
+13
View File
@@ -0,0 +1,13 @@
country,year,value
Australia,2025,65129.7227990563
Brazil,2025,10713.2856869511
Canada,2025,55697.6639660837
China,2025,13861.970224368
France,2025,48985.7307807924
Germany,2025,60496.4350820414
India,2025,2702.47987141553
Italy,2025,43308.6403060733
Japan,2025,35951.0449549304
South Korea,2025,36226.9663637512
United Kingdom,2025,57601.9621201954
United States,2025,90026.5163005744
1 country year value
2 Australia 2025 65129.7227990563
3 Brazil 2025 10713.2856869511
4 Canada 2025 55697.6639660837
5 China 2025 13861.970224368
6 France 2025 48985.7307807924
7 Germany 2025 60496.4350820414
8 India 2025 2702.47987141553
9 Italy 2025 43308.6403060733
10 Japan 2025 35951.0449549304
11 South Korea 2025 36226.9663637512
12 United Kingdom 2025 57601.9621201954
13 United States 2025 90026.5163005744
+34
View File
@@ -0,0 +1,34 @@
{
"source": "https://data.worldbank.org/indicator/NY.GDP.PCAP.CD",
"indicator": "NY.GDP.PCAP.CD (GDP per capita, current US$)",
"fetched_at": "2026-10-25T14:28:24.416415+00:00",
"countries": {
"Australia": "AUS",
"Brazil": "BRA",
"Canada": "CAN",
"China": "CHN",
"France": "FRA",
"Germany": "DEU",
"India": "IND",
"Italy": "ITA",
"Japan": "JPN",
"South Korea": "KOR",
"United Kingdom": "GBR",
"United States": "USA"
},
"funded_by": "ledger (Hermes Agent extract lane, Nous free tier)",
"decoder": {
"AUS": 1,
"BRA": 1,
"CAN": 1,
"CHN": 1,
"FRA": 1,
"DEU": 1,
"IND": 1,
"ITA": 1,
"JPN": 1,
"KOR": 1,
"GBR": 1,
"USA": 1
}
}
+19
View File
@@ -0,0 +1,19 @@
# GDP per capita, current US$ (World Bank, NY.GDP.PCAP.CD)
Source: World Bank API `NY.GDP.PCAP.CD`, per-ISO3 newest record, fetched `2026-10-25T14:28:24.416415+00:00`.
Funded by: ledger (Hermes Agent extract lane, Nous free tier)
| Country | GDP per capita (current US$) | Year |
|---|---|---|
| Australia | 65129.72 | 2025 |
| Brazil | 10713.29 | 2025 |
| Canada | 55697.66 | 2025 |
| China | 13861.97 | 2025 |
| France | 48985.73 | 2025 |
| Germany | 60496.44 | 2025 |
| India | 2702.48 | 2025 |
| Italy | 43308.64 | 2025 |
| Japan | 35951.04 | 2025 |
| South Korea | 36226.97 | 2025 |
| United Kingdom | 57601.96 | 2025 |
| United States | 90026.52 | 2025 |
+222
View File
@@ -0,0 +1,222 @@
World,128003776,118350166,111565144
United States,32642373,30769700,29298000
China,21546403,19498039,18743802
Germany,5471299,5050923,4659929
Japan,4341986,4435163,4026211
United Kingdom,4268446,4002588,3685881
India,4060207,3956067,3952244
France,3554155,3366316,3160443
Russia,2861156,2561310,2173386
Italy,2701737,2551557,2380825
Brazil,2654761,2279920,2185822
Canada,2490256,2319900,2270076
South Korea,2245847,1872375,1875388
Mexico,2176236,1832641,1852723
Australia,2110657,1798519,1830610
Spain,2085276,1906453,1722746
Turkey,1817987,1597293,1323255
Indonesia,1479954,1445643,1396300
Netherlands,1423856,1332768,1214928
Saudi Arabia,1343346,1276943,1254141
Switzerland,1135739,1043530,936564
Poland,1133088,1035492,914696
Taiwan,1019696,n/a,n/a
Belgium,772699,725466,664564
Argentina,768206,683098,638365
Israel,746982,610778,542286
Sweden,745081,668999,604899
Ireland,709972,721701,577389
Singapore,667889,603870,547387
United Arab Emirates,661018,552325,552325
Norway,651712,530756,483593
Austria,616896,579470,534791
Thailand,599115,577010,526518
Colombia,572735,457410,418542
Malaysia,547100,472193,421972
Vietnam,542130,514697,476388
Bangladesh,506357,456319,432742
Philippines,501692,487086,461618
Denmark,499354,462527,424525
South Africa,489083,427184,401145
Romania,470091,428678,382770
Hong Kong,455156,427310,406863
Pakistan,450134,407307,379120
Egypt,442886,365255,306907
Czech Republic,427953,391027,345037
Chile,402243,357371,330267
Peru,399593,334855,289329
Kazakhstan,397921,306239,291480
Nigeria,386671,290794,252084
Portugal,375197,346640,313271
Finland,338564,317039,298697
Algeria,331894,287031,266972
Iran,311950,362682,448092
Greece,304627,280636,257145
New Zealand,273395,264057,260172
Hungary,265911,246490,222905
Cuba,n/a,107352,252063
Iraq,245234,254367,279641
Ukraine,229406,214233,190741
Qatar,203044,215560,219163
Morocco,195632,182374,160606
Uzbekistan,178167,147038,114965
Slovakia,167062,154530,141776
Angola,157297,122175,117159
Kenya,151151,135941,120340
Kuwait,146911,157209,160227
Bulgaria,146507,130777,113343
Dominican Republic,146362,127407,124282
Ecuador,138199,130321,124676
Puerto Rico,133222,129368,125842
Guatemala,132270,123306,113200
Ghana,128638,114210,82938
Ethiopia,127145,126359,142275
DR Congo,122281,91031,75322
Oman,121397,109605,107137
Croatia,115915,105060,92662
Ivory Coast,112115,99774,85894
Serbia,112025,99953,89074
Venezuela,111303,99661,150494
Luxembourg,110417,101158,93280
Costa Rica,109931,102905,95350
Sri Lanka,98964,108825,98714
Lithuania,105907,95210,84869
Belarus,102042,93397,75962
Uruguay,96092,85348,80962
Panama,95024,90463,86260
Tanzania,94889,90143,79236
Slovenia,86732,79648,72485
Myanmar,83832,81666,64796
Turkmenistan,83065,49829,68655
Bolivia,80743,64769,46627
Azerbaijan,78372,75938,74316
Uganda,73370,61986,56369
Cameroon,65135,58933,53297
Jordan,64909,61610,53352
Tunisia,60745,57503,53493
Paraguay,60542,49278,44739
Zimbabwe,56713,51216,45719
Macau,54228,52061,49467
Latvia,53686,48619,43684
Libya,52453,48099,48484
Cambodia,52379,51267,45913
Estonia,51634,47031,42765
Bahrain,48849,48966,47110
Nepal,45844,45490,42692
Cyprus,45171,41226,37635
Sudan,44688,60163,25574
Iceland,43800,38583,33222
Georgia,42716,38144,34192
Honduras,41505,39601,37094
Zambia,41243,28880,25303
Senegal,40469,37007,36715
El Salvador,39838,36708,35365
Haiti,39180,32077,25312
Bosnia and Herzegovina,36771,32600,30478
Lebanon,34497,25972,37990
Papua New Guinea,34403,32499,32262
Guyana,33961,27097,24659
Mali,33847,30069,26588
Albania,33333,30540,27037
Burkina Faso,32513,27627,23250
Armenia,31873,29243,25955
Malta,30712,27772,25030
Guinea,29930,28346,36465
Mongolia,28450,25369,23586
Benin,27786,24566,21483
Trinidad and Tobago,26836,25943,25634
Chad,25628,21473,19906
Niger,24813,21646,17491
Nicaragua,24227,22237,19694
Kyrgyzstan,23606,22624,17478
Gabon,23363,21427,20867
Mozambique,23275,22338,22745
Jamaica,23028,22705,22014
Botswana,21937,19928,19401
Moldova,21889,20352,18200
North Macedonia,21605,19101,16952
Madagascar,21185,19620,17421
Tajikistan,20418,17661,13949
Afghanistan,19662,17779,18283
Laos,18959,18303,16129
Rwanda,18673,16372,15111
Malawi,18309,14918,11734
Namibia,17314,15080,13372
Syria,60043,23738,17254
North Korea,n/a,n/a,17199
Palestine,16017,17167,16017
Mauritius,17119,16158,14953
Bahamas,17042,15833,15833
Congo,17028,16307,16412
Brunei,16863,15032,15463
Mauritania,14352,11680,10912
Somalia,14174,12995,11966
Kosovo,14053,12456,11154
Equatorial Guinea,13722,12823,12288
Togo,13437,11890,10163
Channel Islands,n/a,12507,n/a
Monaco,n/a,11126,11126
Montenegro,10227,9233,8274
Liechtenstein,9442,8906,8628
Bermuda,n/a,9194,8840
New Caledonia,n/a,8549,8549
Barbados,8483,8017,7598
Sierra Leone,8270,7464,6792
Burundi,8137,3365,4431
Maldives,8130,7741,7062
Cayman Islands,n/a,7765,7657
Isle of Man,n/a,7576,n/a
Yemen,7435,21606,14304
Guam,n/a,6910,n/a
Fiji,6352,6198,5968
French Polynesia,n/a,6324,6323
South Sudan,6069,11998,3522
Suriname,5908,4524,4435
Eswatini,5792,5161,4947
Liberia,5642,5246,5251
Andorra,4879,4501,4040
Djibouti,4725,4625,4152
U.S. Virgin Islands,n/a,4672,n/a
Aruba,4671,4168,4192
Faroe Islands,n/a,4053,n/a
Bhutan,3856,3579,3347
Curaçao,n/a,3561,3561
Central African Republic,3492,3066,2752
Greenland,n/a,3327,3466
Belize,3450,3327,3204
Cape Verde,3448,3057,2768
Guinea-Bissau,2985,2528,2008
Lesotho,2972,2574,2391
Gambia,2792,2594,2372
Saint Lucia,2765,2656,2574
Zanzibar,n/a,n/a,2530
San Marino,2417,2027,2082
Antigua and Barbuda,2385,2338,2162
Seychelles,2251,2387,2162
Timor-Leste,2170,1902,1866
Eritrea,1982,2065,1917
Sint Maarten,n/a,1888,1670
Solomon Islands,1836,1750,1584
British Virgin Islands,n/a,n/a,1830
Comoros,1814,1815,1567
Turks and Caicos Islands,n/a,1745,1757
Grenada,1483,1420,1372
Vanuatu,1396,1354,1092
Samoa,1376,1288,1253
Saint Vincent and the Grenadines,1236,1255,1136
São Tomé and Príncipe,1161,981,828
Saint Kitts and Nevis,1143,1184,1122
Northern Mariana Islands,n/a,1096,n/a
American Samoa,n/a,871,n/a
Dominica,791,724,689
Tonga,716,679,648
Saint Martin,n/a,649,n/a
Federated States of Micronesia,521,502,471
Anguilla,n/a,n/a,456
Cook Islands,n/a,n/a,414
Kiribati,401,349,343
Palau,377,345,310
Marshall Islands,342,308,281
Nauru,196,176,187
Montserrat,n/a,n/a,81
Tuvalu,65,57,56
1 World 128003776 118350166 111565144
2 United States 32642373 30769700 29298000
3 China 21546403 19498039 18743802
4 Germany 5471299 5050923 4659929
5 Japan 4341986 4435163 4026211
6 United Kingdom 4268446 4002588 3685881
7 India 4060207 3956067 3952244
8 France 3554155 3366316 3160443
9 Russia 2861156 2561310 2173386
10 Italy 2701737 2551557 2380825
11 Brazil 2654761 2279920 2185822
12 Canada 2490256 2319900 2270076
13 South Korea 2245847 1872375 1875388
14 Mexico 2176236 1832641 1852723
15 Australia 2110657 1798519 1830610
16 Spain 2085276 1906453 1722746
17 Turkey 1817987 1597293 1323255
18 Indonesia 1479954 1445643 1396300
19 Netherlands 1423856 1332768 1214928
20 Saudi Arabia 1343346 1276943 1254141
21 Switzerland 1135739 1043530 936564
22 Poland 1133088 1035492 914696
23 Taiwan 1019696 n/a n/a
24 Belgium 772699 725466 664564
25 Argentina 768206 683098 638365
26 Israel 746982 610778 542286
27 Sweden 745081 668999 604899
28 Ireland 709972 721701 577389
29 Singapore 667889 603870 547387
30 United Arab Emirates 661018 552325 552325
31 Norway 651712 530756 483593
32 Austria 616896 579470 534791
33 Thailand 599115 577010 526518
34 Colombia 572735 457410 418542
35 Malaysia 547100 472193 421972
36 Vietnam 542130 514697 476388
37 Bangladesh 506357 456319 432742
38 Philippines 501692 487086 461618
39 Denmark 499354 462527 424525
40 South Africa 489083 427184 401145
41 Romania 470091 428678 382770
42 Hong Kong 455156 427310 406863
43 Pakistan 450134 407307 379120
44 Egypt 442886 365255 306907
45 Czech Republic 427953 391027 345037
46 Chile 402243 357371 330267
47 Peru 399593 334855 289329
48 Kazakhstan 397921 306239 291480
49 Nigeria 386671 290794 252084
50 Portugal 375197 346640 313271
51 Finland 338564 317039 298697
52 Algeria 331894 287031 266972
53 Iran 311950 362682 448092
54 Greece 304627 280636 257145
55 New Zealand 273395 264057 260172
56 Hungary 265911 246490 222905
57 Cuba n/a 107352 252063
58 Iraq 245234 254367 279641
59 Ukraine 229406 214233 190741
60 Qatar 203044 215560 219163
61 Morocco 195632 182374 160606
62 Uzbekistan 178167 147038 114965
63 Slovakia 167062 154530 141776
64 Angola 157297 122175 117159
65 Kenya 151151 135941 120340
66 Kuwait 146911 157209 160227
67 Bulgaria 146507 130777 113343
68 Dominican Republic 146362 127407 124282
69 Ecuador 138199 130321 124676
70 Puerto Rico 133222 129368 125842
71 Guatemala 132270 123306 113200
72 Ghana 128638 114210 82938
73 Ethiopia 127145 126359 142275
74 DR Congo 122281 91031 75322
75 Oman 121397 109605 107137
76 Croatia 115915 105060 92662
77 Ivory Coast 112115 99774 85894
78 Serbia 112025 99953 89074
79 Venezuela 111303 99661 150494
80 Luxembourg 110417 101158 93280
81 Costa Rica 109931 102905 95350
82 Sri Lanka 98964 108825 98714
83 Lithuania 105907 95210 84869
84 Belarus 102042 93397 75962
85 Uruguay 96092 85348 80962
86 Panama 95024 90463 86260
87 Tanzania 94889 90143 79236
88 Slovenia 86732 79648 72485
89 Myanmar 83832 81666 64796
90 Turkmenistan 83065 49829 68655
91 Bolivia 80743 64769 46627
92 Azerbaijan 78372 75938 74316
93 Uganda 73370 61986 56369
94 Cameroon 65135 58933 53297
95 Jordan 64909 61610 53352
96 Tunisia 60745 57503 53493
97 Paraguay 60542 49278 44739
98 Zimbabwe 56713 51216 45719
99 Macau 54228 52061 49467
100 Latvia 53686 48619 43684
101 Libya 52453 48099 48484
102 Cambodia 52379 51267 45913
103 Estonia 51634 47031 42765
104 Bahrain 48849 48966 47110
105 Nepal 45844 45490 42692
106 Cyprus 45171 41226 37635
107 Sudan 44688 60163 25574
108 Iceland 43800 38583 33222
109 Georgia 42716 38144 34192
110 Honduras 41505 39601 37094
111 Zambia 41243 28880 25303
112 Senegal 40469 37007 36715
113 El Salvador 39838 36708 35365
114 Haiti 39180 32077 25312
115 Bosnia and Herzegovina 36771 32600 30478
116 Lebanon 34497 25972 37990
117 Papua New Guinea 34403 32499 32262
118 Guyana 33961 27097 24659
119 Mali 33847 30069 26588
120 Albania 33333 30540 27037
121 Burkina Faso 32513 27627 23250
122 Armenia 31873 29243 25955
123 Malta 30712 27772 25030
124 Guinea 29930 28346 36465
125 Mongolia 28450 25369 23586
126 Benin 27786 24566 21483
127 Trinidad and Tobago 26836 25943 25634
128 Chad 25628 21473 19906
129 Niger 24813 21646 17491
130 Nicaragua 24227 22237 19694
131 Kyrgyzstan 23606 22624 17478
132 Gabon 23363 21427 20867
133 Mozambique 23275 22338 22745
134 Jamaica 23028 22705 22014
135 Botswana 21937 19928 19401
136 Moldova 21889 20352 18200
137 North Macedonia 21605 19101 16952
138 Madagascar 21185 19620 17421
139 Tajikistan 20418 17661 13949
140 Afghanistan 19662 17779 18283
141 Laos 18959 18303 16129
142 Rwanda 18673 16372 15111
143 Malawi 18309 14918 11734
144 Namibia 17314 15080 13372
145 Syria 60043 23738 17254
146 North Korea n/a n/a 17199
147 Palestine 16017 17167 16017
148 Mauritius 17119 16158 14953
149 Bahamas 17042 15833 15833
150 Congo 17028 16307 16412
151 Brunei 16863 15032 15463
152 Mauritania 14352 11680 10912
153 Somalia 14174 12995 11966
154 Kosovo 14053 12456 11154
155 Equatorial Guinea 13722 12823 12288
156 Togo 13437 11890 10163
157 Channel Islands n/a 12507 n/a
158 Monaco n/a 11126 11126
159 Montenegro 10227 9233 8274
160 Liechtenstein 9442 8906 8628
161 Bermuda n/a 9194 8840
162 New Caledonia n/a 8549 8549
163 Barbados 8483 8017 7598
164 Sierra Leone 8270 7464 6792
165 Burundi 8137 3365 4431
166 Maldives 8130 7741 7062
167 Cayman Islands n/a 7765 7657
168 Isle of Man n/a 7576 n/a
169 Yemen 7435 21606 14304
170 Guam n/a 6910 n/a
171 Fiji 6352 6198 5968
172 French Polynesia n/a 6324 6323
173 South Sudan 6069 11998 3522
174 Suriname 5908 4524 4435
175 Eswatini 5792 5161 4947
176 Liberia 5642 5246 5251
177 Andorra 4879 4501 4040
178 Djibouti 4725 4625 4152
179 U.S. Virgin Islands n/a 4672 n/a
180 Aruba 4671 4168 4192
181 Faroe Islands n/a 4053 n/a
182 Bhutan 3856 3579 3347
183 Curaçao n/a 3561 3561
184 Central African Republic 3492 3066 2752
185 Greenland n/a 3327 3466
186 Belize 3450 3327 3204
187 Cape Verde 3448 3057 2768
188 Guinea-Bissau 2985 2528 2008
189 Lesotho 2972 2574 2391
190 Gambia 2792 2594 2372
191 Saint Lucia 2765 2656 2574
192 Zanzibar n/a n/a 2530
193 San Marino 2417 2027 2082
194 Antigua and Barbuda 2385 2338 2162
195 Seychelles 2251 2387 2162
196 Timor-Leste 2170 1902 1866
197 Eritrea 1982 2065 1917
198 Sint Maarten n/a 1888 1670
199 Solomon Islands 1836 1750 1584
200 British Virgin Islands n/a n/a 1830
201 Comoros 1814 1815 1567
202 Turks and Caicos Islands n/a 1745 1757
203 Grenada 1483 1420 1372
204 Vanuatu 1396 1354 1092
205 Samoa 1376 1288 1253
206 Saint Vincent and the Grenadines 1236 1255 1136
207 São Tomé and Príncipe 1161 981 828
208 Saint Kitts and Nevis 1143 1184 1122
209 Northern Mariana Islands n/a 1096 n/a
210 American Samoa n/a 871 n/a
211 Dominica 791 724 689
212 Tonga 716 679 648
213 Saint Martin n/a 649 n/a
214 Federated States of Micronesia 521 502 471
215 Anguilla n/a n/a 456
216 Cook Islands n/a n/a 414
217 Kiribati 401 349 343
218 Palau 377 345 310
219 Marshall Islands 342 308 281
220 Nauru 196 176 187
221 Montserrat n/a n/a 81
222 Tuvalu 65 57 56
@@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""Verify the GDP-per-capita artifact against a fresh World Bank API fetch.
Re-queries the API for the same 12 economies and diffs the artifact. Exit 0
== artifact matches the live API response; exit 1 == mismatch.
Evidence (this host, fresh API pull):
check: python3 verify_gdp_per_capita.py
"""
import csv
import json
import urllib.request
INDICATOR = "NY.GDP.PCAP.CD"
COUNTRIES = {"Australia": "AUS", "Brazil": "BRA", "Canada": "CAN", "China": "CHN",
"France": "FRA", "Germany": "DEU", "India": "IND", "Italy": "ITA",
"Japan": "JPN", "South Korea": "KOR", "United Kingdom": "GBR",
"United States": "USA"}
OUT_DIR = "/root/troupe-standup/wizard-work/ledger"
def live_value(alpha3):
url = ("https://api.worldbank.org/v2/country/{}/indicator/{}"
"?format=json&per_page=100").format(alpha3, INDICATOR)
with urllib.request.urlopen(url, timeout=30) as r:
data = json.load(r)
results = data[1]
r = results[0] # newest-first
return float(r["value"]), int(r["date"])
def main():
failures = []
# 1) artifact values vs live API values (2 dp)
with open(OUT_DIR + "/gdp-per-capita.csv") as f:
art = {row["country"]: (float(row["value"]), int(row["year"]))
for row in csv.DictReader(f)}
for name, iso in COUNTRIES.items():
live, live_year = live_value(iso)
got, got_year = art.get(name, (None, None))
if got is None:
failures.append("%s: missing from artifact" % name)
continue
if round(got, 2) != round(live, 2) or got_year != live_year:
failures.append("%s: artifact %.2f/%d vs live %.2f/%d"
% (name, got, got_year, live, live_year))
# 2) row count of the table file (data rows only: lines beginning with a
# known country, which excludes the header "| Country ..." and the
# separator "|---|---|---|").
with open(OUT_DIR + "/gdp-per-capita.md") as f:
md = f.read()
countries = set(COUNTRIES)
data_rows = [l for l in md.splitlines()
if any(l.startswith("| " + c) for c in countries)]
n_rows = len(data_rows)
if n_rows != len(COUNTRIES):
failures.append("md data-row count %d != %d" % (n_rows, len(COUNTRIES)))
if failures:
print("FAIL (%d)" % len(failures))
for x in failures:
print(" -", x)
return 1
print("OK: 12/12 economies match the live API; md table row count 12/12.")
return 0
if __name__ == "__main__":
raise SystemExit(main())