Diccionarios de Sinónimos y Antónimos de Federico Carlos Saínz de Robles
  • Python 74.8%
  • JavaScript 15.9%
  • HTML 6.3%
  • Shell 3%
Find a file
ijuanes 8bdc04a5d9 Correct the deploy claims the first real deployment disproved
The unit pointed at `make render`; this repo has no Makefile and never had
one. The build was documented as taking minutes and needing 0.5 GB — measured
on the VPS it is under 20 s (extract 6.8 s, parse 2.3 s, audit 0.3 s, render
4.1 s, precompress 4.0 s) and writes 250 MB (site 229 MB, database 17 MB,
work/ 4.4 MB, beside the 4.3 MB PDF).

ADR 0002 contradicted itself. The 2026-10-16 amendment took binding out of the
decision; the Consequences list still asserted that loopback-only binding was
part of it. Restated as what the amendment actually requires: port 18188 stays
off the internet, and production meets that by passing --host 127.0.0.1 rather
than by relying on serve.py's default.

Step 5 gains the check that cost 20 restarts here. A hand-run serve.py holding
18188 makes the unit start, fail to bind, and be retried (Restart=on-failure,
RestartSec=3) — while the site keeps answering, from the wrong process. The
install looks like it worked and NRestarts is the only thing that says
otherwise.

Troubleshooting gains the two silent failures this deployment produced: a
build that is green at every stage over the one-page extract (the database
guard passes, entries.json is [], and only step 2's checksum catches it), and
a unit that starts while something else serves. The third pitfall records the
rule they both come from — one artefact, one manager — and the three things
that make this service hold to it.
2026-10-17 15:52:56 +00:00
deploy Correct the deploy claims the first real deployment disproved 2026-10-17 15:52:56 +00:00
docs Correct the deploy claims the first real deployment disproved 2026-10-17 15:52:56 +00:00
tests Write the deploy procedure, and fix what it exposed 2026-10-17 09:08:10 +01:00
tools Fix the three ordering faults in preflight --build 2026-10-17 10:38:32 +01:00
.gitignore Add render.py: generate the static site, and the roadmap boundary 2026-10-16 17:01:55 +01:00
.python-version Fix case-sensitive search; pin the interpreter; deploy unit 2026-10-16 18:58:49 +01:00
README.md Write the deploy procedure, and fix what it exposed 2026-10-17 09:08:10 +01:00

Diccionario Español de Sinónimos y Antónimos — search interface

A searchable web interface over the 568-page Diccionario Español de Sinónimos y Antónimos (Federico Carlos Saínz de Robles, Cuban edition, 2012/2014), hosted on a small Debian VPS.

Words in the interface are clickable. A word is a link only if this dictionary has an article for it; the RAE's DLE is a separate, secondary control beside it.

Status: the site is built; the deploy is ready. The extraction and parsing pipeline is complete and validated end-to-end over all 568 pages, producing 24,092 entries and 224,089 relations, and the interface is pre-rendered and searchable. What remains is the manual step on the VPS. See docs/roadmap.md and docs/deploy.md.

Serves on the VPS as sinonimos.ijuanes.ovh, a subdomain proxied by the existing Caddy, with a static file server on port 18188. In production the service binds loopback only, because Caddy is its only client; locally it binds all interfaces by default so a phone on the same network can test it.

Source

File Diccionario-Español-de-Sinónimos-y-Antónimos.pdf
Pages 568 (dictionary body: pp. 11–565)
Origin Adobe InDesign CS4 → Acrobat Distiller 9 (2014)
SHA-256 a549f48fdfd1c34f4d67b51e007a0c37dcb4ac71b09f9ef13231b2b8297d7a19

The PDF is not committed — it lives outside this repo and is referenced by path. Only its checksum is recorded, in docs/source.md.

Quick start

# 1. extract (PDF -> one text file per page, reading order restored)
python3 tools/extract.py --pdf /path/to/book.pdf --out work/pages

# 2. parse (pages -> SQLite)
python3 tools/parse.py --pages work/pages --db data/dictionary.db

# 3. audit the result
python3 tools/verify.py --db data/dictionary.db --pages work/pages

# 4. serve the built site. Binds 0.0.0.0 by default so a phone on the LAN can
#    load it; --precompress writes the .gz siblings it will then send instead
#    of the raw bytes. (Production writes those at build time, not at startup
#    — the service is sandboxed read-only. See docs/deploy.md.)
python3 tools/serve.py --root site --port 18188 --precompress

All tools are dependency-free (standard library + poppler-utils). The Mistral OCR path is not used — see ADR 0001.

Tests

python3 tests/run.py           # 152 tests, stdlib unittest
node tests/search_harness.mjs  # 78 search checks, running the real search.js
python3 tests/mutants.py       # revert each fix; confirm the suite catches it

Seven pipeline bugs were found during development and none of them raised an exception — every one produced plausible, wrong text. tests/mutants.py exists because a test that cannot fail proves nothing: it reverts each fix in turn and asserts the suite goes red. Currently 15/15 mutations caught.

Each test maps to a documented bug in docs/pipeline.md, including two that were found by writing the tests: construction synonyms leaking into the headword, and headwords with parenthetical qualifiers being swallowed into the previous article.

Layout

tools/extract.py     PDF -> work/pages/NNNN.txt   (reading order, dehyphenated)
tools/parse.py       pages -> data/dictionary.db (SQLite)
tools/verify.py      audit invariants on the result
tools/probe_static.py  re-measures the static-site figures below
tools/serve.py       serves the built site on 18188, behind Caddy
tests/               152 tests + a search harness + a mutation check
docs/                design, ADRs, roadmap, deploy, notation reference
deploy/              systemd unit, Caddy block, preflight script
site/                generated static site (gitignored)
data/                generated database (gitignored)
work/                intermediate page text (gitignored)

What is extracted

Metric Value
Entries (articles) 24,092
Synonym relations 210,412
Antonym relations 7,316
Cross-references (V.) 6,361
Construction synonyms (||) 1,264 rows / 1,034 locutions
Distinct related terms ~54,800
Terms flagged as loanwords ((v.)) 14,032
Entries with more than one sense 7,470 (max 23)
Mean synonyms per entry 8.7

The (v.) flag marks cursiva in the original edition — terms that are loanwords, and which the dictionary deliberately marks so they are not confused with native equivalents. The web interface should render these distinctly.

Documentation

Document Contents
docs/notation.md The dictionary's markup language, and how the parser decodes it
docs/pipeline.md Extraction and parsing, and why each step exists
docs/data-model.md SQLite schema and rationale
docs/frontend-backend.md Where the line sits, and the measurements behind it
docs/architecture.md Intended system design for the VPS
docs/roadmap.md Stages, and what is deliberately deferred
docs/decisions.md Settled design decisions, and why
docs/open-questions.md What is still undecided, incl. the RAE-link UX
docs/deploy.md Installing it on the VPS: the artefact, the unit, the Caddy block
docs/source.md Provenance and checksums
docs/adr/ Architecture decision records

How it works

Measured, not assumed (docs/architecture.md):

24,092 articles rendered 2.5–2.9 s
Whole site 90.3 MB raw / 27.4 MB gzipped
Search index 0.76 MB gzipped
…compared to the database 17 MB

The dictionary pre-renders to static files. On the VPS, Python is a build tool and the only running process is a static file server behind the existing Caddy reverse proxy — the same subdomain + automatic-certificate + reverse_proxy shape as every other service. The only JavaScript is search.

Re-check with python3 tools/probe_static.py --db data/dictionary.db. The full deploy procedure is in docs/deploy.md.

Settled

Recorded in full in docs/decisions.md.

  • Backend: Python, build-time only. Go/Rust are unnecessary — there is nothing left running to be fast in.
  • No API. HTML is the only contract between build and browser.
  • Pre-render everything. Smaller than the database it comes from.
  • Vanilla JS, no framework, no build step. A classic defer script, not an ES module: a module fails silently, while the data-state handshake makes a defer script's failure visible (ADR 0003, amended). htmx is a category error without a server; Alpine quadruples a 5 KB JS budget. Plain JS with JSDoc types rather than TypeScript, because TS forces a build step and loses immutable asset caching. Every page stays readable with JavaScript off.
  • Mobile-first, one codebase. One set of pages, one stylesheet, two layouts. Not two frontends.
  • SQLite — 17 MB, 2 cores, 4 GB. Anything heavier is overkill.
  • Sense grouping — synonyms grouped by numbered sense, not listed flat.
  • Fuzzy search — quality of life only, limited to unambiguous tolerance (diacritics, ñ/n).
  • Loanwords — the (v.) flag renders as a badge.
  • Desktop clients — an idea only.
  • Other dictionaries — someday, off the roadmap.

Word clicks are settled — a local article when one exists, with the RAE as a small secondary control (↗, modifier-click) — as is homograph handling. See decisions.md and the remaining open questions.

Licence and attribution

The source text is © Editorial José Martí / Instituto Cubano del Libro, based on Federico Carlos Saínz de Robles (1968). This repository contains tooling and a derived database for personal use; the book itself is not redistributed here.