ledger: add GDP-per-capita (NY.GDP.PCAP.CD) 12-economy table with live-API verifier
This commit is contained in:
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()
|
||||
@@ -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
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 |
|
||||
@@ -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
|
||||
|
@@ -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())
|
||||
Reference in new issue
Block a user