1. Architecture
- Sources14 watched sources: 12 news searches and 2 official pages, covering ID, ASEAN, MY, SG, TH, VN, PH.
- ScannerTwice a week, GitHub Actions runs npm run scan. It fetches, parses, scores and deduplicates.
- TriageOptional. If an API key is set, Claude gives a second opinion on each new headline.
- RadarNew leads are written to data/radar.yml and appear on the Radar page, marked unreviewed.
- ArchiveEvery source without a preserved copy gets one in the Internet Archive, recorded next to the source.
- AuthorThe author opens the source, judges it, and either dismisses the lead or writes an entry.
- GatesEvery change passes the schemas, the validator, the tests and a full build.
- OutcomesEach build computes every promise outcome as of that day and renders the receipts and their share cards.
- SiteA commit to main triggers a Vercel build. The static site updates within minutes.
The design has one rule at its centre: automation may find things, but only the author may publish a claim. The scanner writes to the Radar and nowhere else, and the archiver only adds links to preserved copies beside sources. Entries, instruments and promises change only through a human edit that passes the same checks as every other change. Promise outcomes are never edited at all: they are computed.
2. Technology
| Layer | Choice | Why |
|---|---|---|
| Data | YAML files, JSON Schema 2020-12 | Readable in a diff, editable by hand, versioned in git. |
| Validation | Ajv 8, js-yaml 4, Node.js | Entries and promises are checked against their published schemas. Rules a schema cannot express run in a script before every build. |
| Site | Astro 7, static output | No server, no client framework, no tracking. Small scripts only for filters and the theme. |
| Monitoring | Node.js scanner, GitHub Actions cron | Free, scheduled, logged, and every result is a reviewable commit. |
| Triage | Claude API through the official Anthropic SDK, structured JSON output | Optional second opinion on relevance. The scanner works without it. |
| Share cards | resvg-js, Inter (SIL Open Font License) | Cards drawn as SVG and rendered to PNG during the build, so every card matches the data it was built from. The font is used for the images only, never loaded by the site. |
| Hosting | Vercel, Node 24 | Builds every push to main and serves static files. |
3. Repository map
data/
entries/*.yml one file per entry (the ledger itself)
instruments.yml laws, drafts and guidance, with dossier fields
deadlines.yml public promises, newest first, in the promise format
schema.json JSON Schema for entries
ontology.yml institutions and curated, sourced links
build-log.yml every change asked for, shown below
radar.yml scanner output: unreviewed leads
scan-log.yml one row per scan
watch/sources.yml what the scanner reads
watch/keywords.yml how it scores
watch/seen.json ids already seen, so leads never return
docs/standard/v0.1/ the promise format: spec, JSON Schema, worked examples
kit/ a starting point for a ledger in another country
scripts/
validate.mjs schemas and project rules, run before every build
promises.mjs deadline rules and computed outcomes, used everywhere
archive.mjs preserved copies of sources in the Internet Archive
ontology.mjs fuses all data into one graph of objects and links
make-share-images.mjs link preview cards and icons (resvg, fonts/)
*.test.mjs tests for promises, archive and ontology
scan.mjs the monitoring layer
scan/parse.mjs RSS, Atom and HTML link parsing
scan/score.mjs scoring, deduplication, dash rule
scan/triage.mjs optional Claude second opinion
scan/scan.test.mjs tests on synthetic fixtures
new-entry.mjs creates a dated entry from a template
check-links.mjs reports sources that stopped responding
site/ Astro static site, no client framework
.github/workflows/
ci.yml tests, validation and build on every push
scan.yml the scheduled scan4. Data model
- Entry (schema): id, date, instrument, status, evidence level, titles and summaries in both languages, the seven Safety lens codes with a basis sentence in both languages and the day they were checked, last checked date, EU comparison, the author's take (optional, written only by the author), and at least one source with title, outlet, language and URL. An optional quote must be under 15 words.
- Instrument: id, names, type, status, legal force, a source, and dossier fields: issuer, legal form, key points, EU counterpart, references, and whether the author has read it in full.
- Promise (format, schema): date said, country, who said it and their role, the claim type, for dated promises a deadline rule, deadline and precision, the wording behind the deadline, the promise and what happened since in both languages, an optional outcome date with its source, the evidence level, and the source with its archived copy. There is no outcome field: the outcome is computed.
- Analysis paper (rules): Markdown with front matter (title, author, published and facts-as-of days, version, sources file, summary), numbered sources with access dates and archived copies, and marked quotes. Bahasa Indonesia texts may be partial draft translations.
- Frontier incident (schema): developer, kind, the activity, awareness, notice and disclosure days as sources give them, who saw it first, the authority notified and the channel, countries, summaries in both languages and sources. Day counts are computed by scripts/frontier.mjs.
- Reading item (schema): title, authors, year, kind, url, status (planned, reading, read), and for items read a finish date and the author's notes in both languages.
- Lead: id (a hash of the normalised URL), date found, date published, headline, URL, outlet, country, language, source id, score, matched terms, an optional instrument hint, an optional Claude triage, and a review status: new, reviewed, promoted or dismissed.
5. The scanner
The scanner (scripts/scan.mjs) runs these steps in order:
- Read the source list. 14 sources in data/watch/sources.yml. News searches use the Google News RSS endpoint in the right language edition, limited to the last 30 days. Official pages are read for their links.
- Fetch. Each source is requested with a 25 second timeout and one retry, identifying itself as the Monsoon Ledger scanner. A failed source is logged and skipped. The run fails only if every source fails.
- Parse. A small parser extracts headline, link, outlet, publication date and summary from RSS or Atom, or visible link text from HTML. It keeps only text and absolute http or https links, never markup or scripts.
- Score. Each headline is matched against 38 weighted terms in data/watch/keywords.yml, in Bahasa Indonesia and English. The score is the sum of the weights of the distinct terms found. A lead is kept at a score of 4 or more, only if at least one core subject term (AI or data protection) is among them, so institution names alone never qualify, and unless one of 11 exclusion terms appears.
- Deduplicate. URLs are normalised (host case, tracking parameters, fragments, trailing slashes) and hashed into a ten character id. A lead is skipped if its id was seen before, if the same headline arrived through another URL, or if the story is already a source in the ledger. Because news searches link through redirects, the ledger's own source URLs are also compared by their readable slug.
- Hint and tag. Instrument hint terms suggest which instrument a lead may concern, linked on the Radar. The country tag comes from the headline, then the outlet name, and falls back to the source that found it, so a Malaysian story found by an Indonesian search is tagged Malaysia.
- Triage, optional. With an ANTHROPIC_API_KEY secret set, up to 30 new leads go to Claude in one request. Claude sees only headline, outlet, date and country, and must answer in a fixed JSON schema: relevant or not, event type, instrument, a one sentence restatement of the headline, and confidence. Returned ids and instrument ids are checked against the input, summaries are capped at 30 words, and any error or refusal leaves the leads untriaged. On the site the summary is labelled as a machine summary of the headline.
- Write. New leads are merged into data/radar.yml, newest first. Unreviewed leads are kept 180 days, reviewed ones a year, and the seen index 400 days so a pruned lead never comes back. Em and en dashes in headlines become hyphens, because the project bans them.
The scanner is tested on synthetic fixtures (scan.test.mjs): parsing, scoring, exclusion, deduplication, the dash rule, and that a second run adds nothing new.
6. Promises, receipts and cards
- One outcome module. scripts/promises.mjs derives deadlines from their rules and computes each promise's outcome (met, missed, open or no deadline), its days past or left, and the headline counts. The validator, the ontology, every page, the RSS feeds and the share cards all call it, so a number cannot differ between two places. Outcomes are computed in Western Indonesian Time and never stored.
- As of the build day. Every count states the day it was computed for. A small script in the page layout brings day counts on receipts up to the reader's day. If a deadline has passed since the build, the page keeps its build-day values, which agree with each other, until the next build, at least twice a week through the scan commit.
- Receipts. Each promise has a permanent page at
/en/promises/<key>/and/id/promises/<key>/, where the key is the date said and the speaker, the same key as its ontology object. It shows the wording in Bahasa Indonesia and English, the deadline and its basis, the computed outcome and day count, the evidence, the source and its archived copy, citations in plain text, APA and BibTeX with the data version (the short hash of the last commit that changed data/), and the entries and instruments it connects to. - Share cards. scripts/make-share-images.mjs draws each card as SVG and renders it to PNG with resvg during the site build: one card per receipt per language, plus the site card. Each card shows its outcome, its day count and the day it was made. The same script regenerates the site icons. The full build takes seconds.
- Feeds. The RSS feeds get an item when a promise changes outcome, dated the day after a missed deadline or the day a promise was kept.
- Tests. promises.test.mjs covers leap years, month ends, months counted from the 31st, the deadline day itself, a promise kept on the deadline day, conditional and status claims, and the time zone.
7. Evidence archive
- What. scripts/archive.mjs (
npm run archive) gives every source url in the promises, the instruments, the entries, the analysis sources and the frontier incident reference a preserved copy in the Internet Archive. - How. It asks the Wayback Machine availability API for an existing snapshot first. If there is none, it requests one through Save Page Now, at most eight per run with a twelve second pause between requests.
- Where it writes. Only
archive_urlandarchived_at, beside each source, by editing lines so comments and folded text stay exactly as they were. It never changes any other field. - When. After every scheduled scan. It may fail without stopping the scan or the build; failures are logged in the workflow, and the validator warns about every source still without a copy.
- Tests. archive.test.mjs mocks the network and checks snapshot lookup, saving, rate limiting, failure handling, dry runs and that the file edits touch nothing else.
8. Schedule and deployment
- When. Every Monday and Thursday at 06:17 Western Indonesian Time (cron
17 23 * * 0,3in UTC), so fresh leads are waiting at the start of the working day in Jakarta. Twice a week matches how often Indonesian AI policy news actually moves, without flooding the Radar. - What runs. The scan workflow installs dependencies, runs the tests, runs the scan, archives sources, validates the data and builds the site. Only if the validation and the build pass does it commit radar.yml, scan-log.yml, seen.json and any new archive links to main.
- Publishing. The commit triggers Vercel, which builds and serves the new static site. No server is involved at any point.
- On pull requests. Changes to the scanner run it as a dry run against the live sources, without Claude and without committing, so a change is tested on real feeds before it is merged.
- On demand. The workflow can also be started by hand from the GitHub Actions tab.
9. Quality gates
Every push, by the author or by the scanner, passes four gates in CI: the tests (scanner, promises, ontology, archive, analysis papers, quote check, safety lens, frontier incidents, reading list and language parity), the validator, the JSON Schemas and a full site build, which ends with a parity check: every page must exist in English and Bahasa Indonesia unless it is marked partial. The validator also checks the analysis papers (every citation, every source, https only, no duplicate urls, quote length and one quote per source), the results of the quote check, the safety lens rules, the frontier incident reference and the reading list. npm run links checks every internal link and anchor in the built site. The validator checks entries, instruments, promises, the watched sources and every Radar lead. Promises are checked against the published promise schema, and every deadline is recomputed from its rule. The worked examples in the format's documentation are checked the same way. Sources without an archived copy are reported as warnings, never as failures. Any em or en dash anywhere in the project is rejected. With npm run validate -- --data <folder> the same validator checks only a promise ledger's data folder, such as the kit. The checks applied to entries are listed on the Research protocol page.
10. From lead to entry
- Open the Radar and pick a lead.
- Open the source, then look for the primary text or the official statement it reports, as the verification protocol requires.
- If it does not hold up, set its status to
dismissedin data/radar.yml. If it is worth watching, setreviewed. - If it holds up, run
npm run new-entry -- --slug id-topic, fill every field from the sources you read, and set the lead topromotedwithpromoted_topointing at the new entry id. - Run
npm run validate, commit and push. CI checks it and Vercel publishes it.
11. How Claude was used
Monsoon Ledger was built from an empty repository with Claude Code, Anthropic's coding agent, in cloud sessions linked to the GitHub repository. The division of work is fixed:
- The author decides the scope, chooses what to track, reads the sources, sets every evidence level, writes every "My take", and approves every merge into main.
- Claude writes code and configuration, drafts and translates text for the author to review, checks URLs and legal citations against databases before they are used, and runs the tests, validator and build before each push.
- The rules live in CLAUDE.md, which Claude reads at the start of every session: never invent a fact, quote, date or URL, leave a marked TODO instead, never rewrite the author's own words, and pass the validator. The validator enforces the parts a machine can check.
Build so far. Every update the author asks for gets a row in data/build-log.yml, including fixes, reverts and rollbacks, so the history of the site stays visible. Newest first, each with its pull request and commits.
- 2026-10-27
FeaturePR #8
The ontology explorer becomes a three-layer console
The force-directed graph reshuffled after every scan and turned into a tangle with every object type switched on, because 62 source documents made up half of it. It is replaced by one window with three tabs. Semantic puts objects in fixed lanes in a stable order, groups documents, outlets and references until a selection needs them, and draws only the links around the selected object. Kinetic puts every dated event on one time axis with an event log. Dynamic lists what needs attention on the build day, computed from the data with no forecasts. All three share one inspector, and the window can go full screen. Phones get folding lanes and a bottom sheet. Fixed in testing: a class name clash flattened the window bar, the inspector covered the search and full screen buttons, Escape stopped working after Back, and search missed short names such as GDPR.
- 2026-10-27
ContentPR #7
README, Technical system page and working rules updated for the new layers
The README now lists the new pages, how analysis papers, the quote check, the incident reference and the reading list work, the quality gates and the project layout. The Technical system page describes the new data models and checks. The working rules in CLAUDE.md cover the lens, incidents, reading and quotes. The README paragraph on how AI is used carries a note for the author to confirm, because Claude drafted two entries and the Bahasa Indonesia translation this round.
- 2026-10-27
FeaturePR #7
Language parity check, internal link check and accessibility fixes
The build now fails if a page exists in only one language, unless it is marked partial, or if a data field has English text without its Bahasa Indonesia twin. A new offline check confirms every internal link and anchor in the built site. The automated accessibility check found no contrast or label problems on the new pages. Fixed in testing: a hidden data table made the paper page 722 pixels wide on phones, the menu bar crowded at 1280 pixels with eight items, and two landmark warnings. External links could not be checked from the build environment, which blocks those sites.
- 2026-10-27
- 2026-10-27
FeaturePR #7
Corrections page, two-page brief and contribution guides
A Corrections page lists every changelog line tagged as a correction. A printable two-page brief of Analysis 01 takes its findings and recommendations from the paper's own text and its timeline from the incident data. New contribution guidelines, a code of conduct, a security policy and issue forms for corrections, sources and promises. Discussions and private vulnerability reporting are not switched on yet; that is a repository setting for the author.
- 2026-10-27
FeaturePR #7
Reading list
A reading list for the analysis papers, checked by the validator: an item marked read needs a finish date and the author's notes in both languages. The International AI Safety Report 2026 is being read; four incident reporting reports are planned. No notes were written for the author.
- 2026-10-27
FeaturePR #7
Frontier incident reference
Four public reports of frontier AI models or agents acting on, or used against, systems outside the developer's control, with dates only as sources give them and day counts computed in code. Analysis 01 now reads its 84 and 30 day counts from this data, and the build fails if they drift. Several dates are not given by any source read, among them when OpenAI became aware of the Hugging Face and wiki activity and when the Commission received OpenAI's report; they are left empty.
- 2026-10-27
FeaturePR #7
Safety lens v2 and two entries from published texts
Three new lens questions on every entry: who receives incident reports and whether that body exists, cross-border scope and frontier scope. Every coded value now names its source, and a "no" can no longer come from a press report. The roadmap entry's enforcement value went from "none" to "unclear" because no source states it. New entries for Circular Letter 9/2023 and the ASEAN generative AI guide, without the author's take. New Safety lens page.
- 2026-10-27
FixPR #7
Quote check, verified data fixes, Silencing page off the home page
A quote check reads each source and looks for the quote's exact words; 13 of 13 were found on 27 October 2026, using a local copy of the sources because the build environment cannot reach them. The report-all-incidents quote is now attributed to Aju Widyasari by name and title, a promise row records Irma Handayani's role, and one deadline basis names both readings in its source. The Silencing by automation draft is no longer linked from the home page and is not indexed; its address stays.
- 2026-10-27
FeaturePR #7
Analysis 01, "Nobody to Call", published
The first analysis paper, with numbered references, a figure and promise table computed from the ledger, citation formats, a Word download, Scholar metadata and a draft Bahasa Indonesia translation of its key sections. Before publication its claims were checked against sources: quotes were shortened or cut to one per source, the California and New York rows, the Hugging Face disclosure date and the wiki's name were corrected, and eleven primary sources were added. A wrong reading of 17 October 2024 as the deadline for the data protection institution was corrected on the Instruments and Method pages. Could not be verified: the official text of GR 33/2026, and the population figure in the paper's "Why Indonesia" paragraph, which is marked for the author. Archive copies of the new sources could not be made from the build environment; the scheduled scan will try.
- 2026-10-25
FeaturePR #6
Computed promise outcomes, receipts, the open promise format and evidence archiving
Promise outcomes are now computed from deadlines derived by published rules, so headline counts read among dated promises (4 of 5 missed) with conditional and status claims shown apart, instead of 4 of 7 set by hand. Every promise has a receipt page and share card. The format is published with a JSON Schema the validator uses, worked examples and a kit for other countries. Sources get preserved copies in the Internet Archive after each scan. The weather map and the running clock, which repeated the same seven deadlines, were removed. A GDPR fine tier in the breach entry was corrected.
- 2026-10-23
FeaturePR #5
Visual layer, ontology, About abstract, icons and link previews
A live weather map on the home page whose markers are the seven public deadlines, a running clock since the first promise, a replay slider on Promise vs Reality, charts on Entries and Radar, a reading rail on long pages and motion across the site. An ontology layer joins entries, instruments, promises, officials, institutions and documents, with a graph explorer and a JSON export. The About page became a research abstract, with the author's background taken from the CV and diploma the author shared (professional facts only, the files are not published). New icons and preview cards so links show a proper card in chat apps. The tagline now reads "promises checked against the record". This build log replaced the hand-written timeline.
- 2026-10-23
- 2026-10-23
FeaturePR #4
Monitoring layer, the Radar and the Technical system page
A scanner that reads watched sources twice a week and lists unreviewed leads on a new Radar page, with tests, a scheduled workflow and this page documenting the whole system. Duplicate tables and repeated headings were removed.
- 2026-10-23
- 2026-10-22
FixPR #3
Redesign restored after a deploy from an older branch
A production deploy was made from a branch cut before the redesign, so the live site showed the old design. The redesign was merged back in, the legal brief was ported onto it, and the site was deployed again. The lesson: check main before every deploy.
- 2026-10-22
- 2026-10-22
- 2026-10-22
- 2026-10-22
- 2026-10-22
Feature
Repository, data model, first entries and the bilingual site
Repository layout, licences and CI, the entry schema and validator, instruments, the Janji vs Kenyataan deadlines, the first four entries, the bilingual Astro site, the README, the project rules in CLAUDE.md and the changelog.
12. Keeping it current
- Twice a week, automatic. The scan runs, the Radar updates and the scan history records it.
- Weekly, the author. Review the Radar, dismiss or promote each new lead, and write entries for what holds up.
- When something happens to a promise. Set its outcome_date and outcome_source_url in data/deadlines.yml, update the one-line outcome text, and add a dated line to the changelog. The outcome itself is computed. A new promise gets a row with its claim type, deadline rule and the wording behind it.
- Monthly. Run
npm run links, re-open the sources of entries listed under "due for re-checking" on the Radar, and update their last checked dates. - When coverage drifts. Add or adjust sources and terms in data/watch/. The pull request dry run shows the effect on live feeds before merging.
13. Security and privacy
- No analytics, no cookies, no trackers and no external fonts. The site is static files.
- The only secret is the optional ANTHROPIC_API_KEY, stored as a GitHub Actions secret, never in the repository. Pull request dry runs never use it.
- The scanner workflow has write access to repository contents only, and only on scheduled or manual runs.
- Feeds and pages are treated as untrusted input: only text and links are extracted, and links open with
rel="noopener nofollow". - The scanner collects headlines from public news and official sites. It does not collect social media posts or data about private individuals.
14. Limits of automation
- Headlines only. The scanner reads headlines and feed summaries, not full articles. Relevance is judged on little text.
- Recall and precision. Keyword rules miss stories phrased in unexpected ways and let some noise through. The threshold trades one against the other, and the Radar shows the matched terms so the choice is visible.
- Google News links. News search results link through Google News, which redirects to the outlet. The outlet name is recorded so the original can be found.
- Official sites. Some government pages render content with JavaScript or block automated requests. Those sources fail visibly in the scan history rather than silently.
- Machine summaries. Claude's one line restates a headline. It is not verified and is labelled that way.
- Archive coverage. Some sites refuse the Internet Archive, and Save Page Now limits anonymous requests. Unarchived sources stay flagged until a later run succeeds.
15. Run it yourself
git clone https://github.com/SteveBehren/monsoon-ledger
cd monsoon-ledger
npm run setup # install dependencies (Node 22.23.4 or newer)
npm test # scanner, promise, ontology and archive tests
npm run validate # schemas and project rules
npm run validate -- --data kit/data # check a promise ledger folder only
npm run scan -- --dry-run --no-claude # live scan, writes nothing
npm run archive -- --dry-run # check archive coverage, writes nothing
node scripts/make-share-images.mjs # regenerate the site icons
npm run dev # local site at http://localhost:4321Last scan recorded: Radar