From 506a7e053ddd3d089842d401a4c66db07194e55a Mon Sep 17 00:00:00 2001 From: Greg Burd Date: Fri, 31 Jul 2026 13:07:47 -0400 Subject: [PATCH 1/3] build(docs): per-book PDF output via weasyprint Implement build_pdf(): render each top-level book (2 API refs + 9 guides + articles' 2 sub-books = 13 PDFs) to docs-build/pdf/.pdf. Chapters are concatenated in _meta.toml order (API refs: index first, then alphabetical); a CSS title page carries the live version (dist/RELEASE, 5.3.33) + copyright, with a running header/footer via CSS paged-media (_templates/pdf-print.css). Engine: pandoc(html) -> weasyprint. Chosen over LaTeX because it needs no TeX toolchain, is deterministic, and reuses the existing HTML path. All 13 books render (api_c 655pp, programmer_reference 370pp, ...) in ~3.5 min total. Also fixes two real bugs the link-check surfaced: pandoc_md_to_html now rewrites cross-tree .md links (../../api/c/foo.md) to .html, and build_html copies tree images (docs-src//img/*) into the built site so links resolve. Adds real stub pages for the 2 genuinely-undocumented public APIs (db_env_set_func_assert, db_env_set_win_security) written from their db.h prototypes + source comments, so the completeness gate can be a hard 100%. flake.nix devShell gains the docs validation toolchain (weasyprint, poppler-utils, mandoc, codespell, lychee, write-good) so CI matches local. --- docs-src/_templates/pdf-print.css | 57 ++++++ docs-src/api/c/db_env_set_func_assert.md | 33 ++++ docs-src/api/c/db_env_set_win_security.md | 33 ++++ docs-src/build.py | 203 ++++++++++++++++++++-- flake.nix | 7 + 5 files changed, 321 insertions(+), 12 deletions(-) create mode 100644 docs-src/_templates/pdf-print.css create mode 100644 docs-src/api/c/db_env_set_func_assert.md create mode 100644 docs-src/api/c/db_env_set_win_security.md diff --git a/docs-src/_templates/pdf-print.css b/docs-src/_templates/pdf-print.css new file mode 100644 index 000000000..6ff1d8b7c --- /dev/null +++ b/docs-src/_templates/pdf-print.css @@ -0,0 +1,57 @@ +/* + * Print stylesheet for the libdb PDF books (weasyprint HTML->PDF path). + * + * We render each book to PDF from HTML (the same pandoc html the site uses), + * NOT via LaTeX -- weasyprint needs no TeX toolchain, is deterministic, and + * reuses the pipeline we already have. This file supplies what a LaTeX header + * would: a title page, running header/footer, and page numbers, via CSS + * paged-media (@page). build.py injects the {{project}}/{{version}}/{{book}}/ + * {{copyright}} placeholders into the title block below. + */ + +@page { + size: letter; + margin: 2.2cm 2cm 2cm 2cm; + /* Running footer: book title (left) and page number (right). */ + @bottom-left { content: string(booktitle); font-size: 8pt; color: #666; } + @bottom-right { content: "Page " counter(page); font-size: 8pt; color: #666; } + @top-right { content: "Berkeley DB {{version}}"; font-size: 8pt; color: #666; } +} +/* No header/footer on the title page. */ +@page :first { + @bottom-left { content: ""; } + @bottom-right { content: ""; } + @top-right { content: ""; } +} + +html { font: 11pt/1.45 "DejaVu Serif", Georgia, serif; } +body { margin: 0; } + +/* h1 (chapter titles from pandoc) carry the running-footer book title. */ +h1 { string-set: booktitle "{{book}}"; page-break-before: always; font-size: 18pt; } +h1:first-of-type { page-break-before: avoid; } +h2 { font-size: 14pt; margin-top: 1.4em; } +h3 { font-size: 12pt; } +h4 { font-size: 11pt; } + +pre, code { font-family: "DejaVu Sans Mono", monospace; font-size: 9pt; } +pre { background: #f5f5f5; padding: .6em .8em; border: 1px solid #ddd; + border-radius: 3px; white-space: pre-wrap; word-wrap: break-word; } +code { background: #f5f5f5; padding: 0 .2em; } +pre code { background: none; padding: 0; } +table { border-collapse: collapse; width: 100%; font-size: 9.5pt; } +td, th { border: 1px solid #ccc; padding: .25em .4em; text-align: left; } +a { color: #06c; text-decoration: none; } +img { max-width: 100%; } + +/* Title page: centered project + book + version + copyright. */ +.pdf-title-page { + page-break-after: always; + text-align: center; + /* push the block down the page a bit */ + padding-top: 24%; +} +.pdf-title-page .project { font-size: 30pt; font-weight: 700; margin: 0; } +.pdf-title-page .book { font-size: 20pt; margin: .6em 0 0; } +.pdf-title-page .version { font-size: 14pt; color: #555; margin: 1.2em 0 0; } +.pdf-title-page .copyright{ font-size: 9pt; color: #777; margin: 3em 2cm 0; } diff --git a/docs-src/api/c/db_env_set_func_assert.md b/docs-src/api/c/db_env_set_func_assert.md new file mode 100644 index 000000000..8de668c52 --- /dev/null +++ b/docs-src/api/c/db_env_set_func_assert.md @@ -0,0 +1,33 @@ +--- +title: "db_env_set_func_assert" +api-name: "db_env_set_func_assert" +source: src/dbinc_auto/ext_prot.in +--- +## db_env_set_func_assert + +``` c +#include + +int +db_env_set_func_assert(void (*func_assert)(const char *msg, const char *file, int line)); +``` + +Replace the Berkeley DB call that is invoked when a Berkeley DB assertion fails with **func_assert**. By default, a failed assertion writes a diagnostic message to the error output and aborts the process; **func_assert** lets an application redirect or override that behavior (for example, to log the failure rather than call **abort**). + +The **func_assert** function is called with the text of the failed assertion in **msg**, the name of the source file in **file**, and the line number within that file in **line**. + +The `db_env_set_func_assert()` function configures all operations performed by a process and all of its threads of control, not operations confined to a single database environment. + +Although the `db_env_set_func_assert()` function may be called at any time during the life of the application, it should normally be called before making calls to the db_env_create or db_create methods. + +The `db_env_set_func_assert()` function returns a non-zero error value on failure and 0 on success. + +### Parameters + +#### func_assert + +The **func_assert** parameter is the replacement function. It is called with the failed assertion text (**msg**), source file name (**file**), and line number (**line**). + +### See Also + +Run-time configuration diff --git a/docs-src/api/c/db_env_set_win_security.md b/docs-src/api/c/db_env_set_win_security.md new file mode 100644 index 000000000..3057d006a --- /dev/null +++ b/docs-src/api/c/db_env_set_win_security.md @@ -0,0 +1,33 @@ +--- +title: "db_env_set_win_security" +api-name: "db_env_set_win_security" +source: src/dbinc/globals.h +--- +## db_env_set_win_security + +``` c +#include + +int +db_env_set_win_security(SECURITY_ATTRIBUTES *sa); +``` + +Set the Windows security attributes used by Berkeley DB when it creates the operating-system objects (shared memory and mutexes) that back a database environment. This interface is Windows-specific and has no effect on other platforms. + +On Windows, the objects Berkeley DB creates to implement mutexes are normally initialized by the first Berkeley DB API call that locks a mutex, using the process's default security attributes. If those defaults would make the objects inaccessible to other threads or processes that must share the environment (for example, ones running with lesser privileges), the application may call `db_env_set_win_security()` first to supply an explicit **SECURITY_ATTRIBUTES** structure. + +The `db_env_set_win_security()` function configures all operations performed by a process and all of its threads of control, not operations confined to a single database environment. + +The `db_env_set_win_security()` function must be called before the first Berkeley DB API call that locks a mutex — normally before making calls to the db_env_create or db_create methods. + +The `db_env_set_win_security()` function returns a non-zero error value on failure and 0 on success. + +### Parameters + +#### sa + +The **sa** parameter is a pointer to a Windows **SECURITY_ATTRIBUTES** structure that Berkeley DB applies to the operating-system objects it creates. + +### See Also + +Run-time configuration diff --git a/docs-src/build.py b/docs-src/build.py index c84882175..de78f1713 100644 --- a/docs-src/build.py +++ b/docs-src/build.py @@ -6,12 +6,13 @@ shared template + site.toml, replacing the old per-page duplication. - Nav/index come from per-directory _meta.toml (falls back to a flat listing). -PDF and man outputs are Phase 3/4 — the seams are stubbed below (build_pdf, -build_man) so a follow-up wires pandoc without reshaping this file. +PDF and man outputs are Phase 3/4 — man is implemented; build_pdf renders +one PDF per book via pandoc(html)->weasyprint (no TeX toolchain). -Usage: build.py # build HTML into docs-build/html - build.py --serve # (not implemented) placeholder for a preview seam -Requires: pandoc on PATH (run under `nix shell nixpkgs#pandoc`). +Usage: build.py # build HTML + man + PDF into docs-build/ + build.py --no-pdf # skip PDF (e.g. weasyprint not installed) +Requires: pandoc on PATH; PDF also needs weasyprint +(run under `nix shell nixpkgs#pandoc nixpkgs#python3Packages.weasyprint`). """ import html import re @@ -29,6 +30,8 @@ SITE_TOML = HERE / "_data/site.toml" RELEASE = REPO / "dist/RELEASE" MAN_OUT = REPO / "docs-build/man/man3" +PDF_OUT = REPO / "docs-build/pdf" +PDF_CSS = HERE / "_templates/pdf-print.css" # API .md trees whose refentry pages become section-3 man pages. API_DIRS = [HERE / "api/c", HERE / "api/stl"] @@ -95,8 +98,11 @@ def pandoc_md_to_html(body): ) if p.returncode != 0: raise RuntimeError(f"pandoc md->html failed: {p.stderr[:500]}") - # `.md` links point at source; the built site is HTML. - return re.sub(r'(href="[A-Za-z0-9_.\-]+)\.md(#[^"]*)?"', + # `.md` links point at source; the built site is HTML. Rewrite BOTH + # same-dir (`foo.md`) and cross-tree (`../../api/c/foo.md`) targets, so the + # path charset includes `/`. Skip absolute URLs (http:, //) -- only local + # relative .md links become .html. + return re.sub(r'(href="(?!\w+:|//)[A-Za-z0-9_./\-]+)\.md(#[^"]*)?"', lambda m: f'{m.group(1)}.html{m.group(2) or ""}"', p.stdout) @@ -138,9 +144,29 @@ def build_html(version, site, tmpl): dest.parent.mkdir(parents=True, exist_ok=True) dest.write_text(render_page(tmpl, ctx), encoding="utf-8") n += 1 + _copy_assets() return n +def _copy_assets(): + """Copy tree images to the built site. Pages reference figures bare + (`![](deadlock.jpg)`) but migrate stored them under `/img/`, so + flatten each `img/` into the page dir (docs-build/html//deadlock.jpg). + Without this the /asset links dangle -- the link-check gate catches it.""" + import shutil + for img_dir in SRC.rglob("img"): + if not img_dir.is_dir(): + continue + rel = img_dir.parent.relative_to(SRC) + if rel.parts and rel.parts[0] in SKIP_DIRS: + continue + out_dir = OUT / rel + out_dir.mkdir(parents=True, exist_ok=True) + for asset in img_dir.iterdir(): + if asset.is_file(): + shutil.copy2(asset, out_dir / asset.name) + + # --- Phase 3 seam: man pages (implemented). Phase 4 (PDF) stays stubbed. --- # Man pages skip the sidebar/frameset stub pages and the tree index pages @@ -329,10 +355,132 @@ def _api_groups(): return groups or fb +# --- Phase 4 seam: per-book PDF (weasyprint HTML->PDF; no TeX toolchain). --- +# +# Each top-level "book" (the two API refs + each guide) becomes ONE PDF, its +# chapters concatenated in _meta.toml `order` (API refs have no order -> index +# first, then the rest alphabetically). We render via the SAME pandoc html we +# already produce and hand it to weasyprint, which needs no LaTeX. The title +# page + running header/footer come from _templates/pdf-print.css (CSS paged +# media), which is what a LaTeX header would otherwise supply. +# +# `articles/` has no top-level _meta.toml (it is two independent sub-books, +# inmemory + mssgtxt), so book discovery walks every dir with a _meta.toml +# that names a title -- that yields one PDF per real book, not one monster. + +PDF_SKIP_STEMS = {"frame_index", "frame_main"} + + +def _find_books(): + """Every content dir with a _meta.toml -> (dir, meta). One book per dir.""" + books = [] + for meta_path in sorted(SRC.rglob("_meta.toml")): + d = meta_path.parent + rel = d.relative_to(SRC) + if rel.parts and rel.parts[0] in SKIP_DIRS: + continue + meta = load_meta(d) + if not meta.get("title"): + continue + books.append((d, meta)) + return books + + +def _book_chapters(d, meta): + """Ordered chapter .md paths for a book. `order` pins the reading order; + absent (API refs) -> landing/index first, then the rest alphabetically, + minus the frameset nav stubs.""" + order = meta.get("order") or [] + if order: + paths = [d / f"{s}.md" for s in order] + return [p for p in paths if p.exists()] + landing = meta.get("landing", "index.md") + first = d / landing + rest = [p for p in sorted(d.glob("*.md")) + if p.name != landing and p.stem not in PDF_SKIP_STEMS] + return ([first] if first.exists() else []) + rest + + +def _book_name(d): + """Slug for the output file: guides/gsg_txn -> gsg_txn, api/c -> api_c, + guides/articles/inmemory -> articles_inmemory.""" + return "_".join(d.relative_to(SRC).parts) + + +def _concat_book_md(chapters): + body = [] + for p in chapters: + _meta, txt = strip_front_matter(p.read_text(encoding="utf-8")) + body.append(txt.strip()) + return "\n\n".join(body) + + +def _pdf_css(version, book_title): + """Fill the {{version}}/{{book}} placeholders in the print stylesheet.""" + css = PDF_CSS.read_text(encoding="utf-8") + return (css.replace("{{version}}", version) + .replace("{{book}}", book_title.replace('"', "'"))) + + +def _title_page_html(project, book_title, version, copyright_): + return ( + '
' + f'

{html.escape(project)}

' + f'

{html.escape(book_title)}

' + f'

Version {html.escape(version)}

' + f'' + "
\n" + ) + + +def _book_html(body_md, project, book_title, version, copyright_): + """Standalone HTML for one book: title page + pandoc-rendered chapters, + with the print CSS inlined so weasyprint needs no external files.""" + p = subprocess.run( + ["pandoc", "-f", "gfm", "-t", "html", "--wrap=none"], + input=body_md, capture_output=True, text=True, + ) + if p.returncode != 0: + raise RuntimeError(f"pandoc md->html (pdf) failed: {p.stderr[:500]}") + css = _pdf_css(version, book_title) + title = _title_page_html(project, book_title, version, copyright_) + return ( + "" + f"{html.escape(book_title)}" + f"\n{title}{p.stdout}\n\n" + ) + + +def _weasyprint(html_text, dest): + """HTML string -> PDF file via the weasyprint CLI (stdin '-').""" + p = subprocess.run( + ["weasyprint", "-q", "-", str(dest)], + input=html_text, capture_output=True, text=True, + ) + if p.returncode != 0: + raise RuntimeError(f"weasyprint failed for {dest.name}: {p.stderr[:500]}") + + def build_pdf(version, site): - """TODO(phase-4): pandoc per book (api_reference, GSGs, programmer_reference) - with a shared LaTeX header. Not built this phase.""" - return 0 + """Render every book (2 API refs + each guide) to docs-build/pdf/.pdf. + Returns a list of (name, dest) for the caller to report / page-count. + Requires `pandoc` and `weasyprint` on PATH.""" + if not PDF_CSS.exists(): + sys.exit(f"missing pdf css {PDF_CSS}") + PDF_OUT.mkdir(parents=True, exist_ok=True) + project, copyright_ = site["project"], site["copyright"] + built = [] + for d, meta in _find_books(): + chapters = _book_chapters(d, meta) + if not chapters: + continue + book_title = meta["title"] + body_md = _concat_book_md(chapters) + html_text = _book_html(body_md, project, book_title, version, copyright_) + dest = PDF_OUT / f"{_book_name(d)}.pdf" + _weasyprint(html_text, dest) + built.append((_book_name(d), dest, len(chapters))) + return built def _selfcheck(): @@ -351,10 +499,36 @@ def _selfcheck(): assert "int DB->foo(void);" in mm # synopsis kept # roff tidy drops PP right after a heading assert _tidy_roff(".SS X\n.PP\n.TS\n") == ".SS X\n.TS\n" + # PDF book discovery: every book has a title + at least one chapter, and + # articles is TWO sub-books (not one), so no monster merge. + books = _find_books() + names = {_book_name(d) for d, _ in books} + assert "api_c" in names and "api_stl" in names, names + assert "guides_articles_inmemory" in names, names + assert "guides_articles_mssgtxt" in names, names + assert "guides_articles" not in names, "articles must not be one merged book" + for d, meta in books: + assert meta.get("title"), d + assert _book_chapters(d, meta), f"no chapters for {d}" + # ordered book uses `order`; unordered (api) puts landing first. + prd = SRC / "guides/programmer_reference" + ch = _book_chapters(prd, load_meta(prd)) + assert ch[0].stem == "preface", ch[0] + apic = SRC / "api/c" + assert _book_chapters(apic, load_meta(apic))[0].stem == "index" + # title page HTML carries project + version. + tp = _title_page_html("Berkeley DB", "C API", "5.3.33", "(c) X") + assert "Version 5.3.33" in tp and "Berkeley DB" in tp and "C API" in tp + # cross-tree .md links rewrite to .html; absolute URLs are left alone. + got = re.sub(r'(href="(?!\w+:|//)[A-Za-z0-9_./\-]+)\.md(#[^"]*)?"', + lambda m: f'{m.group(1)}.html{m.group(2) or ""}"', + 'a href="../../api/c/env.md#x" b href="foo.md" c href="http://x/y.md"') + assert '../../api/c/env.html#x' in got and 'foo.html' in got + assert 'http://x/y.md' in got, "absolute .md URL must not be rewritten" print("selfcheck ok") -def main(): +def main(build_pdf_too=True): if not TEMPLATE.exists(): sys.exit(f"missing template {TEMPLATE}") version = load_version() @@ -364,10 +538,15 @@ def main(): print(f"built {n} HTML pages -> {OUT} (version {version})") m = build_man(version, site) print(f"built {m} man pages -> {MAN_OUT} (version {version})") + if build_pdf_too: + books = build_pdf(version, site) + print(f"built {len(books)} PDF books -> {PDF_OUT} (version {version})") + for name, dest, nch in books: + print(f" {name:24s} {nch:4d} chapters {dest.stat().st_size:>9d} bytes") if __name__ == "__main__": if "--selfcheck" in sys.argv: _selfcheck() else: - main() + main(build_pdf_too="--no-pdf" not in sys.argv) diff --git a/flake.nix b/flake.nix index 53803e999..50347ae51 100644 --- a/flake.nix +++ b/flake.nix @@ -50,6 +50,13 @@ pkgs.tcl # for the TCL test harness (--enable-test) pkgs.cbmc # bounded model checker for the formal-verification harnesses (test/cbmc) pkgs.pandoc # docs pipeline: html->md extraction + md->html/pdf/man (docs-src/build.py) + # Docs validation toolchain (docs-src/build.py + .github/workflows/docs.yml): + pkgs.python3Packages.weasyprint # HTML->PDF (build_pdf; no TeX needed) + pkgs.poppler-utils # pdfinfo/pdftotext: PDF page-count + title check + pkgs.mandoc # man -Tlint (0 ERRORS gate) + pkgs.codespell # spelling gate (docs-src/_migrate/spellcheck.py) + pkgs.lychee # internal link-check gate + pkgs.write-good # prose advisory (passive voice / wordiness) ] ++ pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.liburing # Linux io_uring AIO backend (HAVE_IO_URING) ]; From e124e0244a4079f5cc0c372f6f29eba53d60536e Mon Sep 17 00:00:00 2001 From: Greg Burd Date: Fri, 31 Jul 2026 13:08:01 -0400 Subject: [PATCH 2/3] ci(docs): validation workflow with hard gates Add .github/workflows/docs.yml (modeled on ci.yml/fuzz.yml; nix devShell for tool parity). Triggers: push to master, PRs touching docs-src/**, dispatch, and a weekly schedule. HARD gates (block PRs): - build: build.py --no-pdf (HTML + man, 0 errors) + self-check - no-loss: verify_all.py runs verify.py over all 13 migrated trees; fails on any content drop (retention < threshold or a code/section hard drop) - completeness: man_coverage.py --ci; 28/28 public functions documented, and every uncovered method must be on the frozen allowlist (a NEW undocumented API fails). Matcher also taught the dbsite/set_-drop DocBook stem shapes. - spelling: spellcheck.py runs codespell keyed on (path, word), baselined to the ~153 legacy typos so only newly-introduced typos fail - internal link-check: lychee --offline + lychee.toml (deferred-tree and un-migrated-asset links excluded); every migrated internal link must resolve - man-lint: mandoc -Tlint over all .3, 0 ERRORS ADVISORY (continue-on-error): prose (write-good passive/wordiness counts). BEST-EFFORT/scheduled: pdf build + validate_pdf.py (slow), external link-check. Every command was dry-run inside 'nix develop' and passes. --- .github/workflows/docs.yml | 194 +++++++++++++++++++++++ docs-src/PLAN.md | 30 +++- docs-src/_migrate/codespell-baseline.txt | 153 ++++++++++++++++++ docs-src/_migrate/codespell-wordlist.txt | 17 ++ docs-src/_migrate/lychee.toml | 40 +++++ docs-src/_migrate/man_coverage.py | 78 ++++++++- docs-src/_migrate/spellcheck.py | 89 +++++++++++ docs-src/_migrate/validate_pdf.py | 77 +++++++++ docs-src/_migrate/verify_all.py | 65 ++++++++ 9 files changed, 737 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/docs.yml create mode 100644 docs-src/_migrate/codespell-baseline.txt create mode 100644 docs-src/_migrate/codespell-wordlist.txt create mode 100644 docs-src/_migrate/lychee.toml create mode 100644 docs-src/_migrate/spellcheck.py create mode 100644 docs-src/_migrate/validate_pdf.py create mode 100644 docs-src/_migrate/verify_all.py diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..38dafbbb0 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,194 @@ +# Documentation validation. +# +# Builds the modernized docs (docs-src/build.py: Markdown -> HTML + man + PDF) +# and runs the validators that lock in the reverse-DocBook migration's +# guarantees. Modeled on ci.yml/fuzz.yml conventions: hard gates block PRs, +# advisory tiers are continue-on-error, and heavy work (PDF/TeX-free but slow, +# external link check) is scheduled-only or best-effort so per-PR jobs stay +# fast. +# +# Tooling comes from the flake dev shell (nix develop) so CI matches local +# exactly -- pandoc, weasyprint, poppler-utils, mandoc, codespell, lychee and +# write-good are all pinned there. +# +# HARD gates (fail the PR): build (HTML+man), no-loss, completeness, spelling, +# internal link-check, man-lint. +# ADVISORY (continue-on-error): prose (write-good), external link-check. +# BEST-EFFORT (scheduled / continue-on-error): PDF build + validation (slow). + +name: Docs + +on: + push: + branches: [master] + pull_request: + paths: + - 'docs-src/**' + - '.github/workflows/docs.yml' + - 'flake.nix' + - 'dist/RELEASE' + workflow_dispatch: + schedule: + # Weekly (Mon 05:23 UTC): the full run including the slow PDF build and the + # external link check, which are best-effort/skipped on per-PR runs. + - cron: '23 5 * * 1' + +concurrency: + group: docs-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + # ---------------------------------------------------------------------------- + # Build HTML + man (fast, always) and run the hard gates that depend on the + # generated output. PDF is built here only on schedule/dispatch (see the + # `pdf` job) so a PR isn't gated on the ~4-minute weasyprint pass. + # ---------------------------------------------------------------------------- + build: + name: build + gates (html, man, no-loss, completeness, spelling, links, man-lint) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Nix (flakes enabled) + uses: cachix/install-nix-action@v27 + with: + extra_nix_config: | + experimental-features = nix-command flakes + + # 1. BUILD (hard): HTML + man with 0 errors. --no-pdf keeps the PR fast; + # the PDF path is exercised by the `pdf` job (scheduled/best-effort). + - name: Build HTML + man + run: nix develop --command bash -c 'cd docs-src && python3 build.py --no-pdf' + + # build.py self-check guards the md->man reshape, PDF book discovery, and + # the .md->.html link rewrite (unit-level, no external tools). + - name: build.py self-check + run: nix develop --command bash -c 'cd docs-src && python3 build.py --selfcheck' + + # 2. NO-LOSS GATE (hard): every migrated tree still retains its source + # content (word-multiset retention + no code/section drop). Locks the + # "nothing lost" guarantee against future edits. + - name: No-loss gate (all trees) + run: nix develop --command bash -c 'python3 docs-src/_migrate/verify_all.py' + + # 3. COMPLETENESS GATE (hard): every public db.h function is documented + # (28/28), and every uncovered method is on the frozen allowlist -- a + # NEW undocumented API fails. + - name: Completeness gate (API coverage) + run: nix develop --command bash -c 'python3 docs-src/_migrate/man_coverage.py --ci' + + # 4. SPELLING (hard on NEW typos): codespell, baselined against the legacy + # typo backlog so only newly-introduced typos fail. + - name: Spelling gate (codespell, baselined) + run: nix develop --command bash -c 'python3 docs-src/_migrate/spellcheck.py' + + # 6. INTERNAL LINK CHECK (hard): every link into migrated content must + # resolve. Deferred-tree + un-migrated-asset links are excluded (see + # docs-src/_migrate/lychee.toml). External links are the advisory job. + - name: Internal link check (lychee, offline) + run: | + nix develop --command bash -c \ + 'shopt -s globstar; lychee --offline --config docs-src/_migrate/lychee.toml --no-progress "docs-build/html/**/*.html"' + + # 7. MAN-LINT (hard): 0 ERRORS from mandoc across every generated .3 + # (STYLE/WARNING are fine). + - name: Man-lint (mandoc -Tlint, 0 ERRORS) + run: | + nix develop --command bash -c ' + e=0 + for f in docs-build/man/man3/*.3; do + if mandoc -Tlint "$f" 2>&1 | grep -q "ERROR"; then + echo "ERRORS in $f:"; mandoc -Tlint "$f" 2>&1 | grep "ERROR" | head -3 + e=$((e+1)) + fi + done + echo "man pages with ERRORs: $e" + test "$e" -eq 0' + + # ---------------------------------------------------------------------------- + # 5. PROSE (advisory): write-good passive-voice / wordiness counts. This is + # decades-old technical prose -- surface the numbers, never gate. + # ---------------------------------------------------------------------------- + prose: + name: prose advisory (write-good) + runs-on: ubuntu-latest + continue-on-error: true + steps: + - uses: actions/checkout@v4 + - name: Install Nix (flakes enabled) + uses: cachix/install-nix-action@v27 + with: + extra_nix_config: | + experimental-features = nix-command flakes + - name: write-good suggestion counts (per tree) + run: | + nix develop --command bash -c ' + shopt -s globstar + total=0 + for d in docs-src/api/* docs-src/guides/*; do + [ -d "$d" ] || continue + n=$(write-good "$d"/**/*.md 2>/dev/null | grep -c "on line" || true) + printf "%-40s %5s suggestions\n" "$d" "$n" + total=$((total+n)) + done + echo "::notice title=Prose (write-good)::$total advisory suggestion(s) across docs-src"' + + # ---------------------------------------------------------------------------- + # PDF (best-effort / scheduled): weasyprint renders one PDF per book (~4 min + # over all 13). Not a per-PR gate -- runs on schedule/dispatch, or on a PR + # that touches build.py's PDF path. continue-on-error so a rendering hiccup + # informs without blocking. + # ---------------------------------------------------------------------------- + pdf: + name: pdf build + validate (best-effort) + runs-on: ubuntu-latest + continue-on-error: true + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event_name == 'push' + steps: + - uses: actions/checkout@v4 + - name: Install Nix (flakes enabled) + uses: cachix/install-nix-action@v27 + with: + extra_nix_config: | + experimental-features = nix-command flakes + - name: Build all outputs (incl. PDF) + run: nix develop --command bash -c 'cd docs-src && timeout 900 python3 build.py' + - name: Validate PDFs (non-empty, page count, title-page version) + run: nix develop --command bash -c 'python3 docs-src/_migrate/validate_pdf.py' + - name: Upload PDFs + if: always() + uses: actions/upload-artifact@v4 + with: + name: docs-pdf + path: docs-build/pdf/*.pdf + if-no-files-found: ignore + + # ---------------------------------------------------------------------------- + # External link check (advisory / scheduled): lychee WITH network, so it + # depends on the reachability of third-party sites. Never gates; scheduled + # and dispatch only so PRs don't wait on the network. + # ---------------------------------------------------------------------------- + external-links: + name: external link check (advisory) + runs-on: ubuntu-latest + continue-on-error: true + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + steps: + - uses: actions/checkout@v4 + - name: Install Nix (flakes enabled) + uses: cachix/install-nix-action@v27 + with: + extra_nix_config: | + experimental-features = nix-command flakes + - name: Build HTML + run: nix develop --command bash -c 'cd docs-src && python3 build.py --no-pdf' + - name: Check external links (network, advisory) + run: | + nix develop --command bash -c ' + shopt -s globstar + lychee --no-progress --scheme http --scheme https \ + --exclude "localhost" --max-concurrency 8 \ + "docs-build/html/**/*.html" || true' diff --git a/docs-src/PLAN.md b/docs-src/PLAN.md index 3e4c0a161..199a9d0ea 100644 --- a/docs-src/PLAN.md +++ b/docs-src/PLAN.md @@ -126,12 +126,32 @@ the site (and can attach to GitHub releases). 3. **Man pages** (DONE): `build_man()` -> 787 `*.3` (785 C+STL refentry pages + `libdb.3` overview + utilities), `mandoc -Tlint` = 0 errors. `man_coverage.py` reports API coverage (measure-only; the CI gate is phase 5). -4. **PDF** (TODO): pandoc per book (programmer_reference, the GSGs, api_reference) - with a shared LaTeX header. `build.py build_pdf()` is the stubbed seam. -5. **CI** (TODO): `docs.yml` with all validators + the completeness gate (wire - `verify.py` per tree + `man_coverage.py` as hard gates). +4. **PDF** (DONE): `build_pdf()` renders ONE PDF per book (13 books: 2 API refs + + 9 guides + articles' 2 sub-books) via pandoc(html)->**weasyprint** -- no TeX + toolchain, deterministic, ~3.5 min for all 13. Title page (project + live + version + copyright) + running header/footer via `_templates/pdf-print.css` + (CSS paged-media). Output `docs-build/pdf/.pdf`; `validate_pdf.py` + asserts non-empty + sane page count + version on the title page. Page counts: + api_c 655, programmer_reference 370, api_stl 257, installation 168, + upgrading 164, gsg_txn 119, gsg 96, collections 93, gsg_db_rep 66, bdb-sql 47, + mssgtxt 41, inmemory 20, porting 16. +5. **CI** (DONE): `.github/workflows/docs.yml` (nix devShell for tool parity). + HARD gates: build (html+man, 0 errors), no-loss (`verify_all.py`, all 13 + trees), completeness (`man_coverage.py --ci`: 28/28 functions + allowlisted + methods), spelling (`spellcheck.py`, codespell baselined to legacy typos), + internal link-check (`lychee` + `lychee.toml`), man-lint (mandoc, 0 ERRORS). + ADVISORY: prose (write-good). BEST-EFFORT/scheduled: PDF build+validate, + external link-check. The 2 genuinely-undocumented APIs + (`db_env_set_func_assert`, `db_env_set_win_security`) got real stub pages, so + the function gate is a hard 100%. 6. **Publish** (TODO): wire gh-pages to the generated HTML; update the landing - page. + page. Needs: a `pages` job (or step) that runs `build.py`, uploads + `docs-build/html` via `actions/upload-pages-artifact` + `deploy-pages` + (permissions: `pages: write`, `id-token: write`); a top-level `index.md` + linking the 12 book landing pages + the man/PDF outputs; and a decision on + whether PDFs/man are published alongside HTML. The deferred CXX/TCL/java/ + csharp trees stay out until regenerated from their native doc tools (their + inbound links are excluded in `lychee.toml`). ### Phase 2 retention (verify.py, mean word retention; 0 hard drops on all) diff --git a/docs-src/_migrate/codespell-baseline.txt b/docs-src/_migrate/codespell-baseline.txt new file mode 100644 index 000000000..1e7bfef51 --- /dev/null +++ b/docs-src/_migrate/codespell-baseline.txt @@ -0,0 +1,153 @@ +docs-src/api/c/db_heap_rid.md indx +docs-src/api/c/db_sql_codegen.md requre +docs-src/api/c/dbcompact.md re-use +docs-src/api/c/dbget.md retun +docs-src/api/c/dbset_flags.md ACI +docs-src/api/c/dbset_partition.md implimented +docs-src/api/c/dbset_partition.md simultaniously +docs-src/api/c/envclose.md unncessary +docs-src/api/c/envget_create_dir.md ponter +docs-src/api/c/envlog_get_config.md ACI +docs-src/api/c/envlog_set_config.md ACI +docs-src/api/c/envset_flags.md ACI +docs-src/api/c/envset_mp_mtxcount.md defualt +docs-src/api/c/envset_thread_id.md re-use +docs-src/api/c/mempfget.md exlusive +docs-src/api/c/mutexget_init.md inital +docs-src/api/c/mutexset_init.md inital +docs-src/api/c/txnbegin.md ACI +docs-src/api/c/txncommit.md ACI +docs-src/api/c/txncommit.md possiblity +docs-src/api/stl/DbstlDbt.md neccessary +docs-src/api/stl/DbstlDbt.md refered +docs-src/api/stl/DbstlElemTraits.md compatiable +docs-src/api/stl/DbstlElemTraits.md contigous +docs-src/api/stl/DbstlElemTraits.md funcitons +docs-src/api/stl/DbstlElemTraits.md singeleton +docs-src/api/stl/ElementHolder.md Wappers +docs-src/api/stl/ElementHolder.md unerlying +docs-src/api/stl/ElementRef.md Wappers +docs-src/api/stl/Element_wrappers.md Wappers +docs-src/api/stl/db_container.md proctected +docs-src/api/stl/db_map.md unequality +docs-src/api/stl/db_multimap.md unequality +docs-src/api/stl/db_vector.md contaienr +docs-src/api/stl/db_vector.md databse +docs-src/api/stl/db_vector_base_iterator.md explictily +docs-src/api/stl/db_vector_iterator.md explictily +docs-src/api/stl/dbstl_global_functions.md exisiting +docs-src/api/stl/dbstl_helper_classes.md Wappers +docs-src/api/stl/stlDbstlDbtoperator_assign.md neccessary +docs-src/api/stl/stlDbstlElemTraitscompare.md compatiable +docs-src/api/stl/stlDbstlElemTraitscompare.md funcitons +docs-src/api/stl/stlDbstlElemTraitscopy.md compatiable +docs-src/api/stl/stlDbstlElemTraitscopy.md funcitons +docs-src/api/stl/stlDbstlElemTraitseof.md compatiable +docs-src/api/stl/stlDbstlElemTraitseof.md funcitons +docs-src/api/stl/stlDbstlElemTraitseq.md compatiable +docs-src/api/stl/stlDbstlElemTraitseq.md funcitons +docs-src/api/stl/stlDbstlElemTraitseq_int_type.md compatiable +docs-src/api/stl/stlDbstlElemTraitseq_int_type.md funcitons +docs-src/api/stl/stlDbstlElemTraitsfind.md compatiable +docs-src/api/stl/stlDbstlElemTraitsfind.md funcitons +docs-src/api/stl/stlDbstlElemTraitsinstance.md singeleton +docs-src/api/stl/stlDbstlElemTraitslength.md compatiable +docs-src/api/stl/stlDbstlElemTraitslength.md funcitons +docs-src/api/stl/stlDbstlElemTraitslt.md compatiable +docs-src/api/stl/stlDbstlElemTraitslt.md funcitons +docs-src/api/stl/stlDbstlElemTraitsmove.md compatiable +docs-src/api/stl/stlDbstlElemTraitsmove.md funcitons +docs-src/api/stl/stlDbstlElemTraitsnot_eof.md compatiable +docs-src/api/stl/stlDbstlElemTraitsnot_eof.md funcitons +docs-src/api/stl/stlDbstlElemTraitsto_char_type.md compatiable +docs-src/api/stl/stlDbstlElemTraitsto_char_type.md funcitons +docs-src/api/stl/stlDbstlElemTraitsto_int_type.md compatiable +docs-src/api/stl/stlDbstlElemTraitsto_int_type.md funcitons +docs-src/api/stl/stlElementHolderoperator__aa.md bahavior +docs-src/api/stl/stlElementHolderoperator__ma.md bahavior +docs-src/api/stl/stlElementHolderoperator_assign.md bahavior +docs-src/api/stl/stlElementHolderoperator_da.md bahavior +docs-src/api/stl/stlElementHolderoperator_decr.md bahavior +docs-src/api/stl/stlElementHolderoperator_gt_ge.md bahavior +docs-src/api/stl/stlElementHolderoperator_ia.md bahavior +docs-src/api/stl/stlElementHolderoperator_incr.md bahavior +docs-src/api/stl/stlElementHolderoperator_lt_le.md bahavior +docs-src/api/stl/stlElementHolderoperator_modasg.md bahavior +docs-src/api/stl/stlElementHolderoperator_oa.md bahavior +docs-src/api/stl/stlElementHolderoperator_sa.md bahavior +docs-src/api/stl/stlElementHolderoperator_xa.md bahavior +docs-src/api/stl/stlElementRefElementRef.md unerlying +docs-src/api/stl/stldb_mapinsert.md similiar +docs-src/api/stl/stldb_mapoperator_ueq.md unequality +docs-src/api/stl/stldb_multimapoperator_ueq.md unequality +docs-src/api/stl/stldb_vector_base_iteratordstr_db_vector_base_iterator.md explictily +docs-src/api/stl/stldb_vector_base_iteratoroperator_sub.md substract +docs-src/api/stl/stldb_vector_iteratordstr_db_vector_iterator.md explictily +docs-src/api/stl/stldb_vector_iteratoroperator_sub.md substract +docs-src/api/stl/stldb_vectorassign.md requirs +docs-src/api/stl/stldb_vectorunique.md dertermine +docs-src/api/stl/stldbstl_global_functionscommit_txn.md funcion +docs-src/api/stl/stldbstl_global_functionsset_current_txn_handle.md commiting +docs-src/api/stl/stldbstl_global_functionsset_global_dbfile_suffix_number.md exisiting +docs-src/guides/articles/inmemory/index.md desireable +docs-src/guides/articles/mssgtxt/index.md commiting +docs-src/guides/articles/mssgtxt/index.md compresssion +docs-src/guides/articles/mssgtxt/index.md connnect +docs-src/guides/articles/mssgtxt/index.md indx +docs-src/guides/articles/mssgtxt/index.md paritions +docs-src/guides/articles/mssgtxt/index.md spcified +docs-src/guides/articles/mssgtxt/index.md traget +docs-src/guides/bdb-sql/sqlrep.md operatons +docs-src/guides/gsg_db_rep/elections.md desireable +docs-src/guides/gsg_db_rep/fwrkmasterreplica.md applicaton +docs-src/guides/gsg_db_rep/rep_init_code.md peformed +docs-src/guides/installation/build_android_jdbc.md exisits +docs-src/guides/installation/build_unix_conf.md Documenation +docs-src/guides/installation/changelog_4_8.md Millenium +docs-src/guides/installation/changelog_4_8.md hearbeat +docs-src/guides/installation/changelog_4_8.md partically +docs-src/guides/installation/changelog_4_8.md redefinitons +docs-src/guides/installation/changelog_5_0.md Millenium +docs-src/guides/installation/changelog_5_0.md datbase +docs-src/guides/installation/changelog_5_0.md eqivalent +docs-src/guides/installation/changelog_5_0.md mulitple +docs-src/guides/installation/changelog_5_0.md resouces +docs-src/guides/installation/changelog_5_0.md segementation +docs-src/guides/installation/changelog_5_0.md simulatenously +docs-src/guides/installation/changelog_5_0.md teh +docs-src/guides/installation/changelog_5_0.md unitialized +docs-src/guides/installation/changelog_5_1.md datbase +docs-src/guides/installation/changelog_5_1.md explict +docs-src/guides/installation/changelog_5_1.md numer +docs-src/guides/installation/changelog_5_1.md operaton +docs-src/guides/installation/changelog_5_1.md segementation +docs-src/guides/installation/changelog_5_2.md re-use +docs-src/guides/installation/changelog_5_3.md Enhaced +docs-src/guides/installation/changelog_5_3.md begining +docs-src/guides/installation/changelog_5_3.md dependant +docs-src/guides/installation/introduction.md infomation +docs-src/guides/installation/upgrade_11gr2_52_repmgr_channels.md asychronous +docs-src/guides/installation/upgrade_11gr2_52_repmgr_channels.md sychronous +docs-src/guides/installation/upgrade_11gr2_52_xa.md Applictions +docs-src/guides/installation/upgrade_11gr2_remsupp.md Millenium +docs-src/guides/porting/certport.md thrid +docs-src/guides/porting/certport.md warninigs +docs-src/guides/porting/modscope.md envrionment +docs-src/guides/porting/modscope.md platfrom +docs-src/guides/programmer_reference/arch_apis.md extention +docs-src/guides/programmer_reference/bt_conf.md preceeding +docs-src/guides/programmer_reference/ch13s02.md pre-emptive +docs-src/guides/programmer_reference/csharp.md libaries +docs-src/guides/programmer_reference/embedded.md seemlessly +docs-src/guides/programmer_reference/env_encrypt.md Documenation +docs-src/guides/programmer_reference/intro_products.md informaion +docs-src/guides/programmer_reference/lock_max.md enviroment +docs-src/guides/programmer_reference/mp_warm.md intialize +docs-src/guides/programmer_reference/program_perfmon.md stap +docs-src/guides/programmer_reference/program_ram.md re-used +docs-src/guides/programmer_reference/stl_examples.md squre +docs-src/guides/programmer_reference/stl_usecase.md prefered +docs-src/guides/programmer_reference/transapp_atomicity.md ACI +docs-src/guides/programmer_reference/transapp_throughput.md ACI +docs-src/guides/programmer_reference/txn_config.md ACI +docs-src/guides/upgrading/changelog_4_7.md invalide diff --git a/docs-src/_migrate/codespell-wordlist.txt b/docs-src/_migrate/codespell-wordlist.txt new file mode 100644 index 000000000..b5983390a --- /dev/null +++ b/docs-src/_migrate/codespell-wordlist.txt @@ -0,0 +1,17 @@ +# Project wordlist for codespell over the libdb docs. +# +# codespell only flags words in its own typo dictionary, and (verified) it does +# NOT flag Berkeley DB jargon like mpool / DBT / txnid / lsn / btree / recno / +# subdatabase / memp / repmgr / logc. So this list is deliberately short: it is +# for the handful of domain tokens or abbreviations that a future codespell +# dictionary bump might start flagging. Add a term here (lowercased) to teach +# codespell it is correct. One word per line; codespell reads this via +# --ignore-words. +# +# Known BDB terms (kept as documentation even though current codespell passes +# them): mpool, dbt, txnid, lsn, btree, recno, subdatabase, memp, repmgr, logc. +mpool +txnid +subdatabase +recno +btree diff --git a/docs-src/_migrate/lychee.toml b/docs-src/_migrate/lychee.toml new file mode 100644 index 000000000..f8b36ca67 --- /dev/null +++ b/docs-src/_migrate/lychee.toml @@ -0,0 +1,40 @@ +# lychee link-check config for the generated docs site (docs-build/html). +# +# The docs INTERNAL link gate is HARD: every link to migrated content must +# resolve. The excludes below are the only known dangling targets, and NONE is +# a migration regression: +# - CXX / java / com/sleepycat: language-binding API trees deliberately NOT +# migrated (see docs-src/PLAN.md "Deferred"); links into them dangle until +# a future phase regenerates them from source. +# - build_vxworks / build_wince: dead in the UPSTREAM DocBook too (VxWorks / +# WinCE build pages were referenced but never shipped) -- pre-existing, not +# introduced by the migration. +# When a deferred tree is migrated, drop its line here so its links re-enter +# the gate. +# +# Usage (internal only, offline): +# lychee --offline --config docs-src/_migrate/lychee.toml "docs-build/html/**/*.html" +# External links are advisory and checked separately (scheduled), not here. + +exclude = [ + "/api_reference/CXX/", + "/api_reference/TCL/", + "/guides/java/", + "/java/com/sleepycat/", + "build_vxworks", + "build_wince", + # Bundled external assets referenced by the programmer's reference but not + # migrated into docs-src (they live only in the source docs/ tree): the + # USENIX papers (*.pdf), the example C listings (transapp.cs / writetest.cs), + # and a sample tuning file (solaris.txt). A later phase can copy these in; + # until then their links dangle and are not a migration regression. + "_usenix.pdf", + "BDB_Prog_Reference.pdf", + "transapp.cs", + "writetest.cs", + "solaris.txt", +] + +# Internal-only run: skip all network schemes so a PR gate never depends on the +# reachability of external sites (those are an advisory, scheduled job). +scheme = ["file"] diff --git a/docs-src/_migrate/man_coverage.py b/docs-src/_migrate/man_coverage.py index 4324c445c..45de4d69c 100644 --- a/docs-src/_migrate/man_coverage.py +++ b/docs-src/_migrate/man_coverage.py @@ -15,6 +15,7 @@ Usage: man_coverage.py """ import re +import sys from pathlib import Path REPO = Path(__file__).resolve().parents[2] @@ -33,7 +34,7 @@ "__db_sequence": ("seq",), "__db_log_cursor": ("logc",), "__db_channel": ("dbchannel",), - "__db_site": ("repmgr",), + "__db_site": ("dbsite", "repmgr"), } # Internal method-table slots that were never public API and have no DocBook @@ -67,6 +68,42 @@ def ext_functions(): r"((?:db_[a-z_]+)|log_compare) __P", t, re.M))) +# Known method slots with NO dedicated DocBook refentry page. These are the +# historic doc structure, NOT new gaps: +# - getter halves of a set/get pair, documented on the setter page +# (get_bt_compare is on dbset_bt_compare(3), etc.); +# - internal/callback vtable slots never given their own page +# (pget, prdbt, s_callback, stored_*, is_bigendian, version, ...). +# The completeness gate is HARD on functions (must be 100%) and HARD on any +# method that is NOT on this list -- a genuinely new undocumented method fails +# CI. The list is frozen against phase-3's audit (only 2 genuinely-undocumented +# APIs existed, both now stub pages). Shrinking it (documenting one) is fine; +# GROWING it needs a real page or a deliberate edit here. +KNOWN_UNDOCUMENTED_METHODS = { + ("__db", "db_append_recno"), ("__db", "get_append_recno"), + ("__db", "get_assoc_flags"), ("__db", "get_bt_compare"), + ("__db", "get_bt_compress"), ("__db", "get_bt_prefix"), + ("__db", "get_dup_compare"), ("__db", "get_errcall"), + ("__db", "get_feedback"), ("__db", "get_h_compare"), + ("__db", "get_h_hash"), ("__db", "get_msgcall"), + ("__db", "pget"), ("__db", "s_callback"), + ("__db", "set_paniccall"), ("__db", "stored_close"), + ("__db", "stored_get"), + ("__db_env", "get_app_dispatch"), ("__db_env", "get_errcall"), + ("__db_env", "get_feedback"), ("__db_env", "get_isalive"), + ("__db_env", "get_mp_max_openfd"), ("__db_env", "get_mp_max_write"), + ("__db_env", "get_msgcall"), ("__db_env", "get_thread_id_fn"), + ("__db_env", "get_thread_id_string_fn"), ("__db_env", "is_bigendian"), + ("__db_env", "log_put_record"), ("__db_env", "log_read_record"), + ("__db_env", "prdbt"), ("__db_env", "rep_flush"), + ("__db_env", "rep_process_message"), ("__db_env", "set_mp_max_openfd"), + ("__db_env", "set_mp_max_write"), ("__db_env", "set_paniccall"), + ("__db_log_cursor", "version"), ("__db_mpoolfile", "get_last_pgno"), + ("__db_sequence", "get_db"), ("__db_txn", "set_txn_lsnp"), + ("__dbc", "pget"), +} + + def man_stems(): return {p.stem for p in MAN.glob("*.3")} @@ -86,10 +123,19 @@ def matches(prefixes, meth, stems): # DocBook stems collapse the FIRST subsystem underscore but keep the rest: # rep_get_config -> repget_config, rep_stat_print -> repstat_print. first_collapse = meth.replace("_", "", 1) + # Setter pages drop the `set_` entirely: rep_set_config -> repconfig, + # rep_set_transport -> reptransport, set_event_notify -> event_notify + # (matched with a handle prefix below). Try the set_-dropped forms too. + set_dropped = meth.replace("set_", "", 1) if "set_" in meth else None if meth in stems or tail in stems or first_collapse in stems: return True + if set_dropped is not None and (set_dropped in stems + or set_dropped.replace("_", "") in stems): + return True for prefix in prefixes: cands = {prefix + meth, prefix + "_" + meth, prefix + tail} + if set_dropped is not None: + cands |= {prefix + set_dropped, prefix + set_dropped.replace("_", "")} if cands & stems: return True for s in stems: @@ -144,6 +190,36 @@ def main(): if fmiss: print(f"\nfunctions with no matched man page: {', '.join(fmiss)}") + # --ci: HARD completeness gate. Every public function must be covered, and + # every uncovered method must be on the frozen allowlist (a NEW undocumented + # method fails). Exit non-zero on any violation so CI blocks the PR. + if "--ci" in sys.argv: + new_gaps = [(h, m) for (h, m) in missing + if (h, m) not in KNOWN_UNDOCUMENTED_METHODS] + stale = sorted(KNOWN_UNDOCUMENTED_METHODS - set(missing)) + fail = False + if fmiss: + print(f"\nGATE FAIL: {len(fmiss)} public function(s) undocumented: " + f"{', '.join(fmiss)}") + fail = True + if new_gaps: + print(f"\nGATE FAIL: {len(new_gaps)} NEW undocumented method(s) " + "(add a refentry .md, or allowlist deliberately):") + for h, m in new_gaps: + print(f" {h} {m}") + fail = True + if stale: + # A previously-undocumented method is now documented -> tidy the + # allowlist. Advisory (doesn't fail), just nudges the list smaller. + print(f"\nnote: {len(stale)} allowlisted method(s) are now covered; " + "trim KNOWN_UNDOCUMENTED_METHODS:") + for h, m in stale: + print(f" {h} {m}") + if fail: + sys.exit(1) + print("\nGATE PASS: all public functions documented; " + f"all {len(missing)} uncovered methods are known non-page slots.") + if __name__ == "__main__": main() diff --git a/docs-src/_migrate/spellcheck.py b/docs-src/_migrate/spellcheck.py new file mode 100644 index 000000000..071f2fd01 --- /dev/null +++ b/docs-src/_migrate/spellcheck.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +"""Spelling gate: codespell over the docs, hard-failing only on NEW typos. + +The migrated docs carry ~150 legacy typos in decades-old source prose. Fixing +those is a separate content pass (and would churn no-loss-protected content), +so this gate baselines them: it runs codespell, keys each finding on +(relative-path, typo-word), and FAILS only on findings NOT in +codespell-baseline.txt. Any typo an editor introduces on a new/edited page +fails CI; the legacy backlog does not block. Shrinking the backlog (fixing a +baselined typo) is always safe -- a stale baseline entry is reported, not fatal. + +Usage: spellcheck.py # gate (exit 1 on new typos) + spellcheck.py --report # list everything, never fail (advisory) +Requires: codespell on PATH. +""" +import re +import subprocess +import sys +from pathlib import Path + +HERE = Path(__file__).resolve().parent +REPO = HERE.parents[1] +DOCS = REPO / "docs-src" +BASELINE = HERE / "codespell-baseline.txt" +WORDLIST = HERE / "codespell-wordlist.txt" +SKIP = "*.toml,*.tmpl,*.css,_migrate,PLAN.md" + +LINE = re.compile(r"^(.+?):(\d+): ([A-Za-z_-]+) ==> (.+)$") + + +def run_codespell(): + cmd = ["codespell", str(DOCS), f"--skip={SKIP}"] + if WORDLIST.exists(): + cmd.append(f"--ignore-words={WORDLIST}") + p = subprocess.run(cmd, capture_output=True, text=True) + findings = [] # (relpath, line, word, suggestion) + for l in p.stdout.splitlines(): + m = LINE.match(l) + if not m: + continue + path, line, word, sugg = m.groups() + rel = str(Path(path).resolve().relative_to(REPO)) + findings.append((rel, line, word, sugg)) + return findings + + +def load_baseline(): + if not BASELINE.exists(): + return set() + out = set() + for l in BASELINE.read_text().splitlines(): + if "\t" in l: + rel, word = l.split("\t", 1) + out.add((rel.strip(), word.strip())) + return out + + +def main(): + findings = run_codespell() + baseline = load_baseline() + report = "--report" in sys.argv + seen = {(rel, word) for rel, _, word, _ in findings} + new = [(rel, ln, w, s) for (rel, ln, w, s) in findings + if (rel, w) not in baseline] + stale = sorted(baseline - {(rel, w) for rel, _, w, _ in findings}) + + print(f"codespell findings: {len(findings)} " + f"({len(baseline)} baselined legacy typos)") + if report: + for rel, ln, w, s in findings: + print(f" {rel}:{ln}: {w} ==> {s}") + print(f"\nstale baseline entries (now fixed): {len(stale)}") + return + if stale: + print(f"note: {len(stale)} baselined typo(s) no longer present " + "(trim codespell-baseline.txt):") + for rel, w in stale[:20]: + print(f" {rel}\t{w}") + if new: + print(f"\nSPELLING GATE FAIL: {len(new)} NEW typo(s) " + "(fix, or add to codespell-wordlist.txt if a real word):") + for rel, ln, w, s in new: + print(f" {rel}:{ln}: {w} ==> {s}") + sys.exit(1) + print("SPELLING GATE PASS: no new typos beyond the legacy baseline.") + + +if __name__ == "__main__": + main() diff --git a/docs-src/_migrate/validate_pdf.py b/docs-src/_migrate/validate_pdf.py new file mode 100644 index 000000000..907ac8436 --- /dev/null +++ b/docs-src/_migrate/validate_pdf.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""Validate the generated per-book PDFs (docs-build/pdf/*.pdf). + +Asserts each PDF: exists + is non-empty, has a sane page count (>= a floor and +roughly >= its chapter count / a divisor -- a book with N chapters must not +collapse to a couple of pages), and carries the live version string on its +title page (page 1). The version comes from dist/RELEASE, same source as the +build. Exits non-zero on any failure so CI can gate (best-effort in practice -- +see docs.yml). + +Usage: validate_pdf.py [--version 5.3.33] +Requires: pdfinfo + pdftotext (poppler-utils) on PATH. +""" +import subprocess +import sys +from pathlib import Path + +HERE = Path(__file__).resolve().parent +REPO = HERE.parents[1] +PDF_DIR = REPO / "docs-build/pdf" + + +def load_version(): + txt = (REPO / "dist/RELEASE").read_text() + import re + g = lambda k: re.search(rf"^{k}=(\d+)", txt, re.M).group(1) + return f"{g('DB_VERSION_MAJOR')}.{g('DB_VERSION_MINOR')}.{g('DB_VERSION_PATCH')}" + + +def pages(pdf): + out = subprocess.run(["pdfinfo", str(pdf)], capture_output=True, text=True).stdout + for l in out.splitlines(): + if l.startswith("Pages:"): + return int(l.split()[1]) + return 0 + + +def first_page_text(pdf): + return subprocess.run(["pdftotext", "-f", "1", "-l", "1", str(pdf), "-"], + capture_output=True, text=True).stdout + + +def main(): + version = (sys.argv[sys.argv.index("--version") + 1] + if "--version" in sys.argv else load_version()) + pdfs = sorted(PDF_DIR.glob("*.pdf")) + if not pdfs: + sys.exit(f"no PDFs in {PDF_DIR} (run build.py first)") + fails = [] + for pdf in pdfs: + size = pdf.stat().st_size + np = pages(pdf) + tp = first_page_text(pdf) + has_ver = version in tp + has_project = "Berkeley DB" in tp + ok = size > 1024 and np >= 2 and has_ver and has_project + print(f"{pdf.name:34s} {np:5d} pages {size:>9d} B " + f"title[ver={'Y' if has_ver else 'N'} proj={'Y' if has_project else 'N'}]" + f" {'OK' if ok else 'FAIL'}") + if not ok: + why = [] + if size <= 1024: why.append("empty") + if np < 2: why.append(f"too few pages ({np})") + if not has_ver: why.append(f"no version {version} on title page") + if not has_project: why.append("no project name on title page") + fails.append((pdf.name, ", ".join(why))) + if fails: + print(f"\nPDF VALIDATION FAIL: {len(fails)} book(s):") + for name, why in fails: + print(f" {name}: {why}") + sys.exit(1) + print(f"\nPDF VALIDATION PASS: {len(pdfs)} books, all non-empty, " + f"title page carries Berkeley DB {version}.") + + +if __name__ == "__main__": + main() diff --git a/docs-src/_migrate/verify_all.py b/docs-src/_migrate/verify_all.py new file mode 100644 index 000000000..d8a6cecac --- /dev/null +++ b/docs-src/_migrate/verify_all.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""Run the no-loss gate (verify.py) over EVERY migrated tree. + +The source(HTML)->dest(MD) pairs are the authoritative migration map (the same +ones phase 2 migrated); keeping them here means CI calls one command instead of +duplicating the list in YAML. Exits non-zero if ANY tree drops content (a hard +code/sub-section drop, or word retention below --threshold), so CI gates on it. + +Usage: verify_all.py [--threshold 0.97] +""" +import subprocess +import sys +from pathlib import Path + +HERE = Path(__file__).resolve().parent +REPO = HERE.parents[1] +VERIFY = HERE / "verify.py" + +# (old HTML dir, new MD dir) relative to REPO. Guides that migrated the C +# variant point at the C/ subdir; articles is two independent sub-books. +PAIRS = [ + ("docs/api_reference/C", "docs-src/api/c"), + ("docs/api_reference/STL", "docs-src/api/stl"), + ("docs/programmer_reference", "docs-src/guides/programmer_reference"), + ("docs/upgrading", "docs-src/guides/upgrading"), + ("docs/installation", "docs-src/guides/installation"), + ("docs/porting", "docs-src/guides/porting"), + ("docs/gsg/C", "docs-src/guides/gsg"), + ("docs/gsg_txn/C", "docs-src/guides/gsg_txn"), + ("docs/gsg_db_rep/C", "docs-src/guides/gsg_db_rep"), + ("docs/collections/tutorial", "docs-src/guides/collections"), + ("docs/bdb-sql", "docs-src/guides/bdb-sql"), + ("docs/articles/inmemory/C", "docs-src/guides/articles/inmemory"), + ("docs/articles/mssgtxt", "docs-src/guides/articles/mssgtxt"), +] + + +def main(): + thr = ["--threshold", sys.argv[sys.argv.index("--threshold") + 1]] \ + if "--threshold" in sys.argv else [] + failed = [] + for old, new in PAIRS: + p = subprocess.run( + [sys.executable, str(VERIFY), str(REPO / old), str(REPO / new), *thr], + capture_output=True, text=True, + ) + ret = next((l for l in p.stdout.splitlines() + if "mean word retention" in l), "?") + status = "OK" if p.returncode == 0 else "FAIL" + print(f"{old:38s} {ret.replace('mean word retention: ', ''):>8s} {status}") + if p.returncode != 0: + failed.append(old) + # surface the hard-drop detail so CI logs show WHAT was lost. + for l in p.stdout.splitlines(): + if "HARD DROP" in l or l.strip().startswith(("code", "sub-section")): + print(f" {l.strip()}") + if failed: + print(f"\nNO-LOSS GATE FAIL: {len(failed)} tree(s) dropped content: " + f"{', '.join(failed)}") + sys.exit(1) + print(f"\nNO-LOSS GATE PASS: all {len(PAIRS)} trees retained (0 hard drops).") + + +if __name__ == "__main__": + main() From bebbb0c841354eaac91bd2a2278cad3317f4c50e Mon Sep 17 00:00:00 2001 From: Greg Burd Date: Fri, 31 Jul 2026 13:21:52 -0400 Subject: [PATCH 3/3] ci(docs): exclude the dead second.javas link from the link gate am_second.md points at 'second.javas' (a malformed link to a Java API example in the deferred java tree); the file is absent in the upstream DocBook too, so it is a pre-existing dead link, not a migration regression. The flake-pinned lychee 0.24.1 (what CI's nix develop uses) extracts it where an older lychee did not, so add it to the exclude set. Full gate suite re-verified inside 'nix develop' (the exact CI env): 0 link errors, all gates pass. --- docs-src/_migrate/lychee.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs-src/_migrate/lychee.toml b/docs-src/_migrate/lychee.toml index f8b36ca67..657a289ed 100644 --- a/docs-src/_migrate/lychee.toml +++ b/docs-src/_migrate/lychee.toml @@ -33,6 +33,10 @@ exclude = [ "transapp.cs", "writetest.cs", "solaris.txt", + # A dead link in the upstream DocBook: am_second points at "second.javas" + # (a Java API example in the deferred java tree; the filename is even + # malformed). Dead in the source too -- not a migration regression. + "second.javas", ] # Internal-only run: skip all network schemes so a PR gate never depends on the