Skip to content

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog.

Tags v0.1.0 and v0.2.0 predate this file and have no sections.

[Unreleased]

Upgrade notes

  • The templates/api-service-fastapi and templates/dashboard Compose files publish only on localhost (#469) — the CUBRID broker (33000, whose lab dba account has no password) is now 127.0.0.1:33000, and the API (8000) and the Streamlit dashboard (8501) default to 127.0.0.1 too, so a stack started from an earlier file stops answering from other machines. Your browser on the same host is unaffected. To expose the application on purpose, set API_HOST_IP=0.0.0.0 (API) or DASHBOARD_HOST_IP=0.0.0.0 (dashboard) when you run docker compose up; the broker has no such switch. The API template's CUBRID service also gains the named volumes cubrid_data and cubrid_databases: the first docker compose up after updating starts from an empty database (the old data stays in the unnamed volumes of the earlier containers until you remove them), after which docker compose down keeps the data and only docker compose down -v deletes it.
  • The recipe Compose files no longer fix a host or container name, so a volume created by an earlier file needs a one-time recovery (#463) — quickstart/5min-fastapi, quickstart/5min-sqlalchemy, templates/api-service-fastapi and templates/dashboard drop hostname: (cubrid-fastapi, cubrid-sqlalchemy, cubrid-api-service, cubrid-dashboard) and container_name: (cubrid-db, fastapi-service, cubrid-dashboard) and set CUBRID_DB_HOST: localhost on the CUBRID service, as the root lab does since #436. A database created by an earlier file still names its old host in databases.txt and fails to open with -353 in the new container; the data is kept. In that recipe's folder, apply the recovery from GETTING_STARTED.md ("A lab created before the October 2026 lab changes"): docker compose up -d cubrid, rewrite the host to localhost in databases.txt with the sed command shown there, docker compose restart cubrid, then start the stack. Each recipe README repeats these steps. Do not run docker compose down -v to recover; it deletes the data. Containers are now named by Compose, so address them as services (docker compose exec cubrid ..., docker compose logs cubrid); the dashboard upgrade commands that used docker inspect cubrid-dashboard now use docker compose ps -aq cubrid.
  • The root lab no longer has a fixed container name or host name, so an existing lab needs a one-time recovery (#436) — docker-compose.yml drops container_name: cubrid-cookbook and hostname: cubrid-cookbook and sets CUBRID_DB_HOST: localhost, which registers a new database for localhost. The container is now named by Compose (for example cubrid-cookbook-python-cubrid-1): address it as the service, docker compose exec cubrid ..., instead of by the old container name. A database created by an earlier file still names its old host (cubrid-cookbook or a container ID) in databases.txt and fails to open with -353 in the new container; the data is kept, and GETTING_STARTED.md ("A lab created before the October 2026 lab changes") gives the recovery: rewrite the host to localhost and restart, after first copying an 11.4 database from its unnamed volume if the lab predates #385. Do not run make docker-clean or docker compose down -v to recover; they delete the data.
  • make clean, make check and make verify VERIFY_PATHS=... changed meaning (#437) — make clean now removes only generated files, caches and test results (.live-evidence/, __pycache__, .pytest_cache, .ruff_cache, the staged docs pages and site/) and never touches Docker; to delete the lab's containers and volumes, database data included, run the new make docker-clean (make down still stops the lab and keeps the data). make check is now the non-test quality gate (lint, format-check, check-docs, check-coverage) and no longer runs the offline tests: run make check test for the full offline gate, as CI now does. make lint runs only ruff check; ruff format --check moved to the new make format-check, and the new make format is the only target that rewrites files. make verify VERIFY_PATHS=<dirs> on the command line, kept as the golden-only check in #416 / #434, now fails with a one-line message: run make verify-goldens VERIFY_PATHS=<dirs> instead. A bare make verify (an exported VERIFY_PATHS included) is unchanged, the full live verification.
  • make verify is now the full live verification (#416) — the golden-only check that make verify ran before is now make verify-goldens (make verify VERIFY_PATHS=<dirs> on the command line still runs only those goldens). A bare make verify also runs every live pytest suite and needs two dedicated, empty CUBRID databases in CUBRID_TEST_URL and CUBRID_DASHBOARD_TEST_URL besides the make up database; CONTRIBUTING.md ("Live verification") shows how to start them. Run make deps again first: it now installs every example's requirements and pytest-asyncio.
  • A local lab started from the earlier docker-compose.yml needs a one-time recovery (#385) — the new lab file recreates the cubrid-cookbook container with a fixed host name and named volumes, and an existing database then fails to open with -353, because CUBRID recorded the old host name. The data is kept: GETTING_STARTED.md ("A lab created before the October 2026 lab fix") gives the tested recovery for a default 11.2 lab (rewrite the host in databases.txt, then restart) and for an 11.4 lab (copy the database from its unnamed volume into the new named volume first). Do not use docker compose down -v to recover; it deletes the data.

Added

  • Live runs write test-results/verify/ with one versioned summary (#462) — make verify and every make verify-* lane (scripts/live_gate.py) now also write test-results/verify/ (ignored by git, removed by make clean) with the scripts/report_results.py helpers that make test uses: summary.json (schema_version, an overall status of PASS, FAIL or INCOMPLETE, scope/partial, total counts, and per lane its status, whether it executed, counts, JUnit file, logs and reasons; a lane that never ran is INCOMPLETE, "executed": false and in not_run), environment.json (command, Python and platform, package versions, CUBRID server versions with password-hidden URLs, commit, CI run identifiers), one merged JUnit file per pytest lane under junit/<lane>.xml, the lane logs under logs/<lane>/ and summary.md, a Markdown rendering for a CI step summary. The last console line is built from summary.json. make verify starts from an empty directory; a focused lane replaces only its own files and merges the other lanes recorded for the same commit, so consecutive lanes end with one partial summary. Passwords from CUBRID_TEST_URL and CUBRID_DASHBOARD_TEST_URL (raw, percent-encoded or XML-escaped) are now replaced with *** in every file of test-results/verify/ and .live-evidence/, and a test pins that none reaches a result file. .live-evidence/ keeps its layout, files and exit codes for at least one release because smoke-test.yml and the pull-request smoke jobs read and upload it.
  • make test writes JUnit files and a versioned summary.json (#461) — every run of the offline suites now writes test-results/test/ (ignored by git, removed by make clean): one JUnit file per suite under junit/ and summary.json with schema_version, a status of PASS, FAIL or INCOMPLETE, totals and per-suite counts (selected, passed, failed, errors, skipped, xfailed, xpassed, each test counted once with its worst outcome), the suites not run after a failure, and "coverage": {"measured": false}. The run ends with a one-line summary built from the same data. pytest suites use --junitxml; the unittest suites run through the new scripts/unittest_junit.py, which keeps the console output and exit status of python -m unittest and writes the same JUnit shape; the shell-script suite counts as one check. The offline suites measure no coverage, so no coverage.xml is written and no coverage gate is added. The suite list, console output, stop-at-first-failure behavior and exit status are unchanged; scripts/report_results.py (stdlib XML parsing, like scripts/live_gate.py) is reusable by the live side, and tests/test_report_results.py joins make test. CI does not publish the files yet.
  • CUBRIDdb-to-pycubrid migration guide with four runnable recipes (#445) — the new migration/cubriddb-to-pycubrid/ lesson moves a Python application from the official CUBRIDdb C-extension driver to pycubrid, separately from the Java-to-Python guide. Its README covers installation (a pinned cubrid-python source build versus pip install pycubrid, and why the 2014 CUBRID-Python 9.3.0 PyPI archive is no substitute), connection mapping (CCI DSN to keyword arguments, default user public versus dba, autocommit ON versus OFF, threadsafety 2 versus 1, charset), parameter handling (CCI typed binding versus driver-side literal conversion, the pycubrid 1.12 opt-in executemany(..., prepared=True) batch), rows, LOBs, errno/code/sqlstate versus the official args tuple, transaction failures, disconnects with an unknown outcome, the native features ordinary pycubrid does not offer, and decision points that link the dated cubrid-benchmark comparisons without quoting numbers. Official-driver statements are read from cubrid-python v11.3.0.51 and labelled as not run; there is no drop-in or full CCI parity claim. 01_connection_mapping.py, 02_parameterized_crud.py, 03_fetch_and_errors.py and 04_transaction_visibility.py each create a marked cookbook_cdb_* table, refuse to touch an unmarked table of the same name, and drop their table in finally; their goldens run in make verify (pycubrid>=1.6.1, same output on 1.6.1 and 1.13.1). The guide is linked from README.md, llms.txt, GETTING_STARTED.md and the site navigation (staged as migration-cubriddb-to-pycubrid), with a Korean translation in docs/ko/migration-cubriddb-to-pycubrid.md. SUPPORT_MATRIX.md gains a "CUBRIDdb-to-pycubrid migration" row (4 recipes; 105 in total, 69 golden-backed), and THIRD_PARTY_LICENSES.md lists the new requirements.txt.
  • Partial golden and lane runs are labeled PARTIAL (#460) — make verify-goldens (scoped by VERIFY_PATHS or not) now ends with PARTIAL: checked only the goldens under: <roots> after the unchanged Results: line, and a focused lane (make verify-quickstart, verify-performance, verify-web, verify-django-celery, verify-ai-agent, verify-dashboard, verify-templates) prints PARTIAL: checked only the <lanes> lane(s), repeats it in its summary.txt and records "partial": true in .live-evidence/<lane>/summary.json. The full make verify is the only run that records "partial": false (in its lane summaries and verify-summary.json) and its goldens part prints no PARTIAL line (scripts/verify_goldens.sh reads VERIFY_SCOPE=full, set only by scripts/live_gate.py all; an ambient value is ignored). No golden or suite is added, removed or skipped. tests/test_live_gate.py and tests/test_make_commands.py pin the label, the evidence field and that a scoped run cannot carry the full marker.
  • FastAPI quickstart GET /health and live CRUD tests (#420) — quickstart/5min-fastapi/app.py gains GET /health, which runs SELECT 1 FROM db_root on a new connection and returns {"status":"ok","database":"ok"}, or HTTP 503 with {"status":"error","database":"unavailable"} and no error details when the database cannot be reached. The mocked suite covers both answers. The new quickstart/5min-fastapi/tests/test_live.py drives the app with TestClient against a running CUBRID named by CUBRID_TEST_URL and checks each answer against the table through a separate pycubrid connection: startup and /health, POST /items with the committed row, GET /items and GET /items/{id}, 404 for a missing id, 422 for three invalid bodies with no row written, a rejected over-long insert that leaves no row, and 503 from a real connection attempt to a closed port. get_conn() now passes connect_timeout=10, so /health answers 503 instead of hanging on an unreachable host. Tests write only rows marked with a random live-qs- prefix and delete exactly those, so repeated runs pass. make test leaves test_live.py out, so it never needs or writes to a database; run on their own, the live tests skip without CUBRID_TEST_URL, while a configured but unreachable database fails. make verify-quickstart (and the full make verify) runs them with CUBRID_LIVE_GATE=1 (a missing URL also fails) and writes JUnit and a summary to .live-evidence/quickstart/ (ignored by git). The quickstart README, its Korean page and SUPPORT_MATRIX.md document the endpoint and the command. CI does not run the live suite yet.
  • Release verification also runs on Python 3.11 (#238) — smoke-test.yml takes the Python version from a new python matrix axis (["3.12"]). A release verification (repository_dispatch, a workflow_dispatch pinned to a package, or a workflow_call from a driver's release workflow) adds exactly one cell, CUBRID 11.4 on Python 3.11, the supported minimum, running the same full job (make verify, AI-agent, Flask/FastAPI, async-worker, Django and dashboard suites, offline guards). Push to main, the daily schedule and latest-release manual runs keep the two Python 3.12 cells, so no schedule and no other run costs more; the release adds one job (about the length of an 11.4 cell). The caller contract is unchanged: same inputs and outputs, the existing job names (Smoke Tests (CUBRID 11.2) / (CUBRID 11.4)) and artifact names stay, the new cell is Smoke Tests (CUBRID 11.4, Python 3.11) with the artifact release-verification-part-cubrid-11.4-py3.11. Each report matrix entry gains a python field (scripts/release_smoke.py summary --python), and a failure reason names the cell (CUBRID 11.4, Python 3.11: …); a release report now also fails unless exactly the three expected cells (11.2/3.12, 11.4/3.12, 11.4/3.11) each reported once, naming any missing, duplicate or unexpected cell, and every cell must have passed with the requested version (this also closes the earlier gap where a missing 11.2 or 11.4 part went unnoticed); non-release runs are unchanged, and the Python is left out of a cell name for the default 3.12. Job hygiene is unchanged (60-minute timeout, pinned actions, persist-credentials: false, contents: read). tests/test_release_verification.py and tests/test_release_dispatch.py fail if the extra cell is wrong or unconditional, the expected cell set drifts from the workflow matrix, a cell is missing or duplicated, the report matrix is not sorted, summary --python is not recorded, the Python is hard-coded again, the artifact names collide, or inputs. reaches a step. CONTRIBUTING.md and SUPPORT_MATRIX.md describe the cell.
  • Stricter guard tests for the driver lanes (#239, #241 review follow-up) — tests/test_ci_scope.py now asserts exact lines instead of substrings: the caller's if: github.event_name == 'schedule', the activity-guard comparison (-ge "$ACTIVE_DAYS"), ACTIVE_DAYS: "8", the commit check ok &= got == want and the report job's default-branch clause. A new test ties the driver-main.yml RECIPES list to the directories of scripts/driver_floors.py FLOOR_SETS (parsed, not hard-coded), so both lanes cover the same recipes. The workflow comment and CONTRIBUTING.md document that an open tracking issue is closed only by the next run that executes, and that a manual dispatch closes it when both drivers are idle. No workflow logic changed.
  • Whole-second timestamps normalize like fractional ones (#265) — scripts/normalize_output.sh now maps {{DATE}} HH:MM:SS and {{DATE}}THH:MM:SS with no fractional part to {{DATETIME}}, so goldens such as fundamentals/pycubrid/09_serial_order_numbers.py no longer fail about 1 run in 1000 when a DATETIME lands on .000 and Python's str(datetime) drops the fraction. Values followed by a UTC offset, Z or an uppercase zone name (the fixed 20_timezone_datetime outputs) stay unscrubbed, and every existing golden normalizes unchanged. scripts/test_normalize_output.sh covers whole-second, millisecond and microsecond variants for both separators.
  • Documented driver floors are installed and run, not only checked as text (#241) — the new Driver floors (CUBRID 11.4) job in ci.yml runs scripts/driver_floors.py, which installs every documented pycubrid/sqlalchemy-cubrid floor exactly (==, read from the recipes' own requirements.txt) in one virtual environment per floor set and diffs 60 representative goldens against live CUBRID 11.4 on Python 3.12: the global floor (pycubrid==1.6.1, sqlalchemy-cubrid==1.0), the advanced SQLAlchemy floor (1.4.2, fundamentals/pandas), fundamentals/async, the 1.7 line (fundamentals/connect, orm-basics, lob-handling), the 1.9 line (fundamentals/pycubrid, fundamentals/sqlalchemy) and fundamentals/alembic. It is one job (about 3 minutes) selected by pull requests that change SUPPORT_MATRIX.md, the floor checker or lane, or a lane directory's requirements.txt, by the existing weekly ci.yml schedule and by manual ci.yml runs; it does not run on pushes to main, adds no schedule and is enforced by CI Gate only when selected. The golden directories without a requirements.txt (fundamentals/crud, error-handling, lob-handling, transactions) are now covered, and scripts/check_dependency_floors.py accepts extras (sqlalchemy-cubrid[pycubrid]>=1.0) and spaced specifiers. tests/test_dependency_floors.py fails when a documented floor is not installed by any floor set, a directory-specific floor runs on another directory, the lane pins drift from the requirements, or a floor change would not select the lane. Floors raised where the lane failed: fundamentals/async now needs sqlalchemy-cubrid>=1.5 (1.4.x allows SQLAlchemy 2.1, where its async adapter calls the removed await_ attribute; 1.5.0 is the first passing release), and fundamentals/lob-handling is documented as needing pycubrid>=1.7 (1.6.x returns CLOB values with escaped newlines).
  • Startup smoke test for the root FastAPI template app (#243) — templates/api-service-fastapi/tests/test_root_app.py imports the real app.main, runs its lifespan and checks /health through FastAPI's TestClient, using a throwaway SQLite DATABASE_URL so it needs no CUBRID. Previously CI only byte-compiled app/, so an import error or a missing dependency such as pydantic-settings (declared in the template's requirements.txt but absent from the shared smoke install list) could ship. Both the required pytest-suites lane in ci.yml (selected by any templates/api-service-fastapi/* change) and the non-PR smoke-test.yml suites now install the template's own requirements.txt and run this suite in its own process. tests/test_ci_scope.py covers the lane selection and workflow wiring. SQLite does not exercise CUBRID-specific SQL; the recipe suites continue to do that.
  • make deps and a time-limited, classified make verify (#235, #236) — make deps installs pytest, sqlalchemy-cubrid[pycubrid] and the requirements.txt of every golden-backed example (a directory owning expected/) in one pip resolver call that honours PIP_CONSTRAINT; the discovery lives in the new scripts/example_requirements.py, which scripts/release_smoke.py install-examples now shares. It validates every path before printing any, and make deps fails (installing nothing) when a golden-backed requirements.txt path contains whitespace. make verify first checks that the selected examples' requirements are installed (failing once with "run make deps first"), then runs each script through the new portable, POSIX-only scripts/run_example.py (stdin from /dev/null, own process group; the whole group, including grandchildren, is killed on timeout, when the script exits and when the runner gets SIGINT, SIGTERM or SIGHUP; exit 124 plus a run_example: timed out after line on timeout; no GNU timeout; on a non-POSIX system it exits with "run_example.py requires POSIX process groups") under VERIFY_TIMEOUT seconds (default 60; the slowest golden took 7.2 s on CUBRID 11.4). Results are classified as ✓ PASS, ✗ MISMATCH (diff), ✗ EXEC-ERROR (exit status and last 20 lines; 128 + N for a script killed by signal N), ⏱ TIMEOUT (last 20 lines; only when exit 124 comes with the runner's timeout line, so a script that itself exits 124 is an EXEC-ERROR), ✗ NORMALIZE-ERROR, ✗ READ-ERROR or ? SKIP; the summary keeps N passed, N failed, N skipped and adds a count per failure class, and under GitHub Actions each failure emits an ::error file=...:: annotation. GETTING_STARTED.md, docs/quickstart.md and docs/ko/getting-started.md now install sqlalchemy-cubrid[pycubrid] in section 2, list the ai-agent template, and verify a checkout with a Python 3.11+ venv → make up → make deps → make verify; CONTRIBUTING documents both targets. tests/test_make_commands.py covers the make deps file list (pip replaced by a recorder) and its whitespace failure, the timeout and each failure class, a script exiting 124, a script killed by SIGTERM (exit 143), /dev/null stdin, and that no script or grandchild outlives a timeout, a normal exit, or SIGTERM/SIGHUP to the runner.
  • Selectable local CUBRID version (#169) — CUBRID_VERSION=11.4 make up selects the root Compose image while retaining the 11.2 default and the existing bounded readiness helper. Version-switch instructions no longer require editing YAML, a fixed sleep, or automatic volume deletion; image selection is not a database-format upgrade.
  • Advisory lane against pycubrid and sqlalchemy-cubrid main (#239) — the new .github/workflows/driver-main.yml installs both drivers from their main branches (pinned to the commits resolved at the start of the run; direct_url.json is checked after install) together with the requirements of 15 representative golden directories, then runs make verify on them against live CUBRID 11.4 on Python 3.12, so a driver regression shows up before it is released instead of after. It is advisory only: the recipe job has continue-on-error, no pull request runs it, and CI Gate does not depend on it. A failure opens or updates one tracking issue (ci: cookbook recipes failing against driver main, label ci), which is closed automatically when the recipes pass again — the reporting pattern of sqlalchemy-cubrid's upstream canary. No new schedule: the existing weekly ci.yml schedule calls it (job driver-main-advisory), skipped when neither driver's main changed in the last 8 days, and it can be dispatched manually. It runs only in the upstream repository. tests/test_ci_scope.py fails if the lane joins the gate, gains its own schedule or a pull-request trigger, loses continue-on-error, or stops installing the drivers from their main commits.
  • Representative Python compatibility matrix (#130) — required PR/main CI checks Python 3.11–3.14 on CUBRID 11.4 using the existing read-only pycubrid connection and SQLAlchemy engine/session goldens, plus imports and source compilation. This small matrix does not duplicate the full Python 3.12 smoke/framework suite, and failure, cancellation or unexpected skip blocks the CI gate.
  • Offline contributor command — make check combines lint, documentation, normalizer, golden-coverage, and isolated offline regression suites; make test-offline runs the suites without a database. CI uses the same command, and both live smoke jobs run the Make/readiness guards before driver selection.
  • AI agent template (#111) — templates/ai-agent/ with 5 examples: agent state management (sessions, messages, tool calls with JSON columns), MCP tool chain (programmatic server invocation), RAG metadata hybrid (document store with SET tagging + SEQUENCE chunk tracking), agent loop (query → think → act → observe), and AI chatbot backend (SQLAlchemy ORM with JSON LLM I/O). Requires cubrid-mcp-server>=0.3.
  • One-command dashboard demo — templates/dashboard/ now ships its own docker-compose.yml (CUBRID 11.4 + Streamlit at http://localhost:8501), and the five recipes read DATABASE_URL from the environment (default unchanged: cubrid+pycubrid://dba@localhost:33000/testdb) so the compose service can point them at the co-located container.
  • Streamlit dashboard pytest suite (#103) — templates/dashboard/tests/test_dashboard.py uses streamlit.testing.v1.AppTest to exercise all 5 dashboard recipes: a parametrized smoke test that every recipe runs without raising, plus assertions that the seeded table viewer renders rows, the KPI page's four metric cards are non-empty, and narrowing the category filter shrinks the rendered dataframe. The suite runs against a live CUBRID instance when CUBRID_TEST_URL is set and otherwise falls back to a shared SQLite file for local/offline runs; conftest.py makes the recipes' CUBRID/MySQL-style AUTO_INCREMENT DDL SQLite-compatible via a dialect-gated SQLAlchemy event hook, without changing the recipes themselves, and drops the demo tables in FK-safe order before and after the session. SUPPORT_MATRIX.md's "Streamlit templates" row moves from "manual run" to this pytest suite. This replaces the unwired, CWD-relative tests/test_dashboard.py added by #120.
  • Gate the Streamlit dashboard pytest suite in CI (#142) — Every pull request runs templates/dashboard/tests in its own required live-CUBRID 11.4 job, separate from the Flask/FastAPI database. On pushes to main, nightly, and manual smoke runs, each existing 11.2/11.4 matrix job first finishes the Flask/FastAPI suites, then stops their container and runs the dashboard suite in a separate disposable CUBRID container of the same version. This removes the shared-schema FK failure (cookbook_sales references cookbook_products) without deleting another recipe's tables or relying on suite order. The dashboard's own fixture still drops its tables in FK-safe order, and its SQLite-only AUTO_INCREMENT hook is scoped to its test URL and removed even when teardown fails. SUPPORT_MATRIX.md and docs/index.md describe the trigger-specific coverage and isolation.
  • Live async-worker database task checks (#129) — templates/async-worker/tests invokes the existing sales aggregation, report, and batch email tasks directly without Redis, then confirms CUBRID persisted their job states, JSON results, reports, and email logs. The suite requires an empty dedicated database, fails on a configured connection error or preexisting worker tables, and removes only its own tables. A separate required CUBRID 11.4 PR job and the non-PR 11.2/11.4 smoke matrix gate this database path; broker delivery and Celery's result backend remain manual coverage.
  • Live Django SQLAlchemy-bridge checks (#128) — templates/django/tests exercises the existing Django HTTP /health and /items views against CUBRID, then updates and deletes the persisted row through the template's SQLAlchemy bridge. It does not add a native Django CUBRID backend, ORM model, or migration. The suite rejects non-CUBRID or unreachable configured URLs and preexisting cookbook_items, cleans up only its own table, and runs in a separate required CUBRID 11.4 PR job plus the non-PR 11.2/11.4 smoke matrix after colliding examples.
  • CUBRID-distinctive SQL feature recipes (6 new pycubrid scripts):
  • fundamentals/pycubrid/17_window_functions.py — ROW_NUMBER/RANK/DENSE_RANK, LAG, running SUM over partitions (with deterministic tie-breakers)
  • fundamentals/pycubrid/18_recursive_cte.py — WITH RECURSIVE number series and hierarchy path building (contrast with 08 CONNECT BY)
  • fundamentals/pycubrid/19_pagination.py — LIMIT/OFFSET vs CUBRID-idiomatic FOR ORDERBY_NUM() BETWEEN, plus the ROWNUM caveat
  • fundamentals/pycubrid/20_timezone_datetime.py — DATETIMETZ/DATETIMELTZ native reads and SET TIME ZONE; TIMESTAMPTZ rendered via server-side TO_CHAR to sidestep pycubrid#289
  • fundamentals/pycubrid/21_enum_type.py — ENUM declaration-order sorting, col + 0 ordinal, out-of-set rejection
  • fundamentals/pycubrid/22_date_formatting.py — TO_CHAR/TO_DATE date and number formatting with visible fixed-width padding
  • v1.6.x feature recipes (8 new scripts):
  • fundamentals/async/ — pycubrid.aio + SQLAlchemy async engine
  • fundamentals/alembic/ — programmatic Alembic migration with CubridImpl
  • fundamentals/json/ — native JSON columns, JSON_EXTRACT/UNQUOTE patterns
  • fundamentals/isolation-levels/ — 6 CUBRID levels + dirty-read demo
  • fundamentals/sqlalchemy/07_collection_types.py — SET/MULTISET/SEQUENCE ORM
  • fundamentals/pycubrid/15_cursor_memory_bound.py — fetch_size + tracemalloc
  • fundamentals/pycubrid/16_batch_error_handling.py — executemany_batch error paths
  • Python examples: FastAPI, Django, Flask, SQLAlchemy, pycubrid, Pandas, Celery, Streamlit (already in the v0.1.0 tag, which predates this changelog)
  • llms.txt for AI agent discoverability
  • PRD with Example-first Design Philosophy

Changed

  • The template Compose files no longer publish the CUBRID broker on all interfaces, and the API template keeps its database (#469) — see the Upgrade note. tests/test_quickstart_config.py pins that no quickstart or template Compose file publishes the broker without a 127.0.0.1 host IP and that the API template's CUBRID service mounts both data paths. The READMEs (and the dashboard's Korean page) say what is published and how to reset the data.
  • The recipe Compose files drop their fixed host and container names (#463) — see the Upgrade note. Published ports and their exposure are unchanged (the two quickstarts bind 127.0.0.1; the API-service and dashboard templates keep their existing all-interface 33000/8000/8501 bindings). tests/test_quickstart_config.py now fails if a quickstart or template Compose file sets hostname: or container_name:, or a CUBRID service loses CUBRID_DB_HOST: localhost.
  • Canonical Docker lifecycle targets and a portable lab service (#436) — make docker-up starts the root Compose service cubrid (database testdb) and waits for it, make docker-down stops and removes its containers and network and keeps the volumes, and make docker-clean (unchanged) also deletes the volumes; make up and make down remain aliases, and make demo uses docker-up. The broker is published on 127.0.0.1:${CUBRID_TEST_PORT:-33000}: CUBRID_TEST_PORT moves the lab, for example to run a second one under its own COMPOSE_PROJECT_NAME, while the examples, their goldens and the examples-database lanes still connect to localhost:33000. CUBRID_DB_HOST: localhost replaces the fixed host name, so a container recreated by docker-down and docker-up reopens the kept database; this was verified live on CUBRID 11.2 and 11.4 (a row written before docker-down read back after docker-up in a new container), as was the recovery of labs from the #385 file (11.2 and 11.4) and of a pre-#385 11.4 lab. scripts/agent_eval.py run --live probes the image and cubrid_rel through the Compose service instead of the cubrid-cookbook container name and drops an inherited CUBRID_TEST_PORT. README, GETTING_STARTED.md, CONTRIBUTING.md, SUPPORT_MATRIX.md, AGENTS.md, the first-cubrid and quickstart pages, the fundamentals READMEs and their Korean pages show the lifecycle commands and the service name. tests/test_make_commands.py pins the Compose contract (one cubrid service, testdb, the CUBRID_VERSION image, the localhost port binding, CUBRID_DB_HOST, no container_name/hostname, both named volumes), runs the lifecycle targets and their aliases against a fake docker, and fails if a tracked file still addresses the old container name.
  • Makefile targets have predictable semantics (#437) — make test (alias make test-offline) is one scripts/run_offline_tests.py invocation; the script holds the suite list, runs each suite in its own process as before and now also runs make test-normalize. The make verify-goldens recipe is one scripts/verify_goldens.sh invocation: the golden-run and result-classification shell moved there unchanged (per-golden lines, VERIFY_RESULTS rows, ::error annotations, timeouts, the Results: summary and exit status). make help groups the targets into database, setup and cleanup, offline tests, live verification, quality checks and documentation. ci.yml runs make check test, driver-main.yml runs make verify-goldens VERIFY_PATHS=..., and scripts/agent_eval.py run --live cleans up with make docker-clean (or make clean on a task base SHA that predates it). The focused make verify-* lanes are unchanged.
  • make test and make verify are the two test commands (#416) — make test runs the unit and mocked suites with no database or Docker (make test-offline stays as an alias, and make check runs it). make verify without VERIFY_PATHS is now the full live verification: scripts/live_gate.py all first checks every database URL, server and example dependency, then runs every golden (make verify-goldens, the former golden-only make verify) and every live pytest suite in a fixed order, one at a time: the FastAPI quickstart, migration and performance on the make up database (localhost:33000/testdb, which the examples hard-code), the Flask/FastAPI, Django/Celery and AI agent suites on CUBRID_TEST_URL, and the dashboard on CUBRID_DASHBOARD_TEST_URL. Both URLs must name dedicated, empty databases, distinct from each other and from the make up database. It fails closed on an unreachable database, a SQLite or other non-cubrid+pycubrid URL, a missing dependency, a skipped or empty suite, a suite that leaves or drops tables, and a failed golden, and writes .live-evidence/verify-summary.json with golden and pytest results reported separately, the CUBRID server, Python and package versions and the commit. make verify VERIFY_PATHS=<dirs> keeps the golden-only check of those roots and now says it is partial. make deps installs pytest-asyncio (which the FastAPI recipe suites import) and every example's requirements.txt (golden-backed examples and the live suites) in one pip call. The live lanes added earlier in this release are now feature-named make verify-* targets (verify-quickstart, verify-performance, verify-web, verify-django-celery, verify-ai-agent, verify-dashboard, verify-templates), the gate variable is CUBRID_LIVE_GATE and evidence goes to .live-evidence/<lane>/. make demo runs verify-goldens.
  • The performance benchmarks print the workload they ran (#423) — performance/fetch-optimization/benchmark.py prints the rows each fetch strategy returned, performance/bulk-insert/benchmark.py prints a SELECT COUNT(*) of the rows each strategy stored (taken after its timer stops) instead of the configured count, and performance/connection-pooling/benchmark.py prints how many connections each strategy opened (SQLAlchemy connect event). Timings, strategies and table cleanup are unchanged; the three READMEs describe the new output.
  • The FastAPI and SQLAlchemy quickstart labs behave like the root lab (#386) — quickstart/5min-fastapi/docker-compose.yml and quickstart/5min-sqlalchemy/docker-compose.yml now publish their ports on 127.0.0.1 only (the lab's dba account has no password), give the database container a fixed host name (cubrid-fastapi, cubrid-sqlalchemy) and keep the database in two named volumes (cubrid_data, cubrid_databases), so docker compose down keeps the data and only docker compose down -v deletes it. The FastAPI api service gains a health check on /openapi.json: without it, docker compose up --wait returned while Uvicorn was still starting and the first request got Connection reset by peer. The image (cubrid/cubrid:11.2), the database testdb, the port numbers and the apps are unchanged. A quickstart container created from the earlier files kept its data in unnamed volumes; to start clean, run docker compose down -v in that quickstart's folder before you update. tests/test_quickstart_config.py fails if a quickstart publishes a port beyond 127.0.0.1, loses its fixed host name or a named volume, or the FastAPI API loses its health check.
  • The outbox recipe's ack and fail require the lease attempt (#339) — in templates/flask/08-transactional-outbox, POST /api/outbox/<id>/ack and /fail identified the caller's lease only by processor_id and compared attempts with the value the request read, so a stale request sent after the same processor_id leased the message again settled the newer lease. Request change: both endpoints now require lease_attempt, the attempts value /outbox/lease returned (a missing value returns 409, a boolean or other non-integer value 400). The conditional UPDATE compares attempts with that value, a stale attempt returns 409 ("this lease is stale") and leaves the newer lease untouched, and /fail decides dead-letter versus requeue from it. New tests lease as worker-1 (attempt 1), expire the lease, lease again as worker-1 (attempt 2) and send the attempt-1 ack and fail (409, attempt 2 intact, no attempt row; 200 on the previous code), then settle attempt 2 normally; the in-window race test also runs with a same-processor re-lease, and a test covers the 400 cases. The recipe README documents the request body.
  • Docs-sync allowlist entries must record a reason (#174) — scripts/check_docs_sync.py load_allowlist() used to drop everything after # and keep the path, so an entry with no reason (performance/foo) was silently accepted although scripts/docs-sync-allowlist.txt asks for one. It now follows scripts/check_expected_coverage.py: blank lines and full-line # comments are still ignored, path # reason is accepted (the reason may contain further #), and an entry without a reason or with an empty one raises an error naming the file and line, which makes the gate exit 1 instead of reporting success. tests/test_docs_sync.py covers each case, the non-zero exit and the repository's own allowlist; the allowlist header and CONTRIBUTING.md state the rule.
  • Compose files drop the obsolete version key (#173) — the root docker-compose.yml, quickstart/5min-fastapi/docker-compose.yml, quickstart/5min-sqlalchemy/docker-compose.yml and templates/api-service-fastapi/docker-compose.yml no longer start with version: "3.8" / "3.9". Compose v2 ignores the key and warns that it is obsolete; docker compose config now prints no warning. Services, images, ports and volumes are unchanged.
  • Flask recipes 02 and 07 get their own table names (#277) — Flask 02 (02-categories) now creates cookbook_flask_categories (its self-referencing and product ForeignKey strings follow) instead of cookbook_categories, which the root FastAPI template app keeps, and Flask 07 (07-vendor-feed) now creates cookbook_feed_products instead of cookbook_products, which Flask 01 keeps. No recipe shares a table with another any more, so the FastAPI root app with Flask 02, and Flask 01 with Flask 07, can run their live-CUBRID suites concurrently against one instance. Routes and API responses are unchanged. A database created by an earlier version keeps the old tables; the two recipes create new ones, so drop the old Flask 02 and Flask 07 tables by hand if they are no longer needed. KNOWN_SHARED in tests/test_table_prefix.py is now empty, so the guard is a strict no-sharing check across recipes; SUPPORT_MATRIX.md drops the sequential-run advice for these suites (the dashboard suite still needs its own database because it reuses cookbook_products).
  • FastAPI recipes 09-12 and Flask recipes 09-11 prefix their tables with cookbook_ (#139) — the 23 tables these seven recipes created without a prefix (orders, users, roles, documents, inventory_items, …) are now cookbook_<name>, with the 12 matching ForeignKey strings, as AGENTS.md and CONTRIBUTING.md already required. Generic names could collide with a reader's own tables. inventory_items was shared by FastAPI recipe 09 and Flask recipe 11, so FastAPI 09 now uses the distinct cookbook_saga_inventory_items (Flask 11 keeps cookbook_inventory_items), and likewise cookbook_saga_orders (FastAPI 09) and cookbook_rbac_documents (Flask 09) avoid the names FastAPI 02 and FastAPI 07 use and their live-CUBRID suites can run concurrently against one instance. Routes, URL paths and API responses are unchanged. A database created by an earlier version keeps its old tables; the renamed recipes simply create new ones, so drop the old tables by hand if they are no longer needed. The new tests/test_table_prefix.py (in make test-offline) fails when any __tablename__ or Table("…") in templates/**/models.py lacks the prefix or when a new table name is declared by two recipes. templates/ai-agent/ (agent_*, rag_*, chat_* tables, with a test and README that name them) is left for a separate change.
  • Five more topic recipes are thin redirects to the canonical pycubrid recipes (#149) — fundamentals/connect/01_connect.py, fundamentals/crud/02_crud.py, fundamentals/transactions/03_transactions.py, fundamentals/error-handling/05_error_handling.py and fundamentals/lob-handling/06_lob.py were byte-for-byte copies of fundamentals/pycubrid/01_connect.py, 02_crud.py, 03_transactions.py, 05_error_handling.py and 06_lob.py, goldens included, so make verify counted each recipe twice. Following the #79 approach, the topic entries now print a pointer to the canonical recipe, their expected/ goldens are removed (fundamentals/error-handling keeps its three other goldens), and connect, crud, transactions and lob-handling are allowlisted in scripts/docs-sync-allowlist.txt; topic paths and discovery are unchanged. Because the redirects import nothing, fundamentals/connect/requirements.txt is removed, and the driver-floor lane (scripts/driver_floors.py), the driver-main.yml recipe list, scripts/check_dependency_floors.py, scripts/ci_scope.py and SUPPORT_MATRIX.md no longer list the removed goldens (coverage of connect, CRUD, transactions and LOB at the 1.6.1 and 1.7 floors is dropped: the canonical recipes require pycubrid>=1.9 and run only in the 1.9 line). The Recipe Coverage table drops the combined "Connect, CRUD, errors, transactions, LOB" row (8 goldens) for an "Error-handling topic" row (3), taking the totals from 102 recipes / 66 golden-backed to 97 / 61.
  • Performance figures re-measured on pycubrid 1.10.0 (#245) — the figures under performance/ quoted pycubrid 0.5.0+16a8634 (undated) on CUBRID 11.2. The fetch, bulk-insert and connection-pooling numbers are now the median of 5 runs of each recipe's own benchmark.py on pycubrid 1.10.0 / sqlalchemy-cubrid 1.10.0 / CUBRID 11.4.6 (Intel i5-9400F, Python 3.12, 2026-10-09), each with its range. The "19% faster than a fetchone() loop" claim is removed: on 1.10.0 fetchall() and a fetchone() loop both took 52 ms for 10K rows. The 0.5.0 before/after and row-scaling tables are kept but labelled historical, since the scripts cannot reproduce them.
  • Collection recipes show current read and bind behaviour (#242) — fundamentals/sqlalchemy/07_collection_types.py no longer says collection columns read back as opaque bytes and cannot be bound: it adds ?decode_collections=true to the engine URL (pycubrid 1.2.0+), binds Python lists/sets to the SET/MULTISET/SEQUENCE columns with plain insert()/update() (sqlalchemy-cubrid 1.9 wraps them in pycubrid 1.9's typed Set/Multiset/Sequence parameters) and reads frozenset/list values with select(), replacing the hand-built {...} literals and the TABLE() unnest read helper. fundamentals/pycubrid/10_collection_columns.py connects with decode_collections=True, binds Set/Multiset/Sequence through ? placeholders (including empty collections and a typed SUBSETEQ filter), and prints the decoded column values instead of reading only predicate results through a bytes/latin1 workaround. Goldens for both were regenerated and checked on CUBRID 11.4. Floors raised: fundamentals/pycubrid now needs pycubrid>=1.9,<2 (was >=1.8,<2) and fundamentals/sqlalchemy needs pycubrid>=1.9,<2 and sqlalchemy-cubrid>=1.9 (were >=1.6.1 and >=1.4.2), because typed collection binding first shipped in pycubrid 1.9.0; SUPPORT_MATRIX.md, CONTRIBUTING.md, scripts/check_dependency_floors.py and its test record the new floors. Reading with decode_collections alone would have needed only the older floors. templates/ai-agent keeps its pycubrid>=1.7 floor and default raw-bytes connection; its comment and README no longer point at a removed limitation. Both recipes avoid the known CUBRID collection/NUMERIC IF crash (pycubrid#675).
  • Flask 01-11 and FastAPI 09-12 templates no longer use deprecated datetime.utcnow (#246) — each recipe now produces naive UTC through datetime.now(timezone.utc).replace(tzinfo=None) in a local helper (naive_utc_now(), or utcnow_naive() in FastAPI 12), and the *_at values the Flask 04, 05, 07 and 10 apps and the FastAPI 09 routes wrote explicitly as aware datetimes now use it too, so stored timestamps are consistently naive UTC like CUBRID DATETIME columns. This also fixes FastAPI 09-saga POST /orders/{order_key}/recover, which subtracted a naive stored updated_at from an aware clock and raised TypeError; it now has tests for the 409 and success paths. FastAPI 11-rate-limiter compared an aware utcnow() against naive stored window starts; its route-level utcnow() now delegates to models.naive_utc_now. Column types are unchanged. tests/test_template_timestamps.py (run by make test-offline) fails on any .utcnow attribute under templates/ and runs every helper under a non-UTC TZ, requiring a naive result within seconds of the real UTC time.
  • License inventory records the pinned docs tools (#253) — scripts/build_license_inventory.py no longer lists mkdocs-material and pymdown-extensions unpinned in TOOLING; it reads .github/requirements-docs.txt as its own DOCS requirement set, so THIRD_PARTY_LICENSES.md records the versions docs.yml and the ci.yml docs-site job install (mkdocs 1.6.1, mkdocs-material 9.7.7, pymdown-extensions 12.1). Regenerated with the recorded --exclude-newer 2026-10-09T00:00:00Z: no package or version changed, only the set column of the docs tools and their transitive dependencies (TOOLING becomes DOCS). tests/test_third_party_licenses.py now fails if a docs tool reappears in TOOLING, if a pin is not exact, or if an inventoried docs-tool version differs from its pin. Transitive docs dependencies stay resolved, not pinned.
  • Pinned documentation build tools (#247) — .github/requirements-docs.txt pins mkdocs, mkdocs-material and pymdown-extensions to the versions the strict site build uses today. docs.yml and the ci.yml docs-site job both install from it, so an upstream release can no longer break mkdocs build --strict without a repository change, and Dependabot now updates the pins. A stdlib test guards the file and both install steps. No theme or content change.
  • Django template floor raised to the supported LTS (5.2) (#244) — templates/django/requirements.txt and SUPPORT_MATRIX.md now require django>=5.2: the supported LTS (5.2); 4.2/5.0 are past upstream end of support. The template uses only JsonResponse, path(), csrf_exempt and the test client, so manage.py check and the /health and /items views were checked offline (SQLAlchemy engine swapped for SQLite) on Django 4.2, 5.0 and 5.2 with no difference. The live CUBRID bridge job still installs the latest Django.
  • Least-privilege workflow tokens and no persisted checkout credentials (#237) — ci.yml now declares top-level permissions: contents: read; the reusable doc-lint call explicitly grants the contents: read and pull-requests: read that workflow declares. Every actions/checkout step in every workflow sets persist-credentials: false. The Dependabot auto-merge job now also requires github.event.pull_request.user.login == 'dependabot[bot]' (not only the actor) and holds its contents: write / pull-requests: write grants at job level only. tests/test_workflow_permissions.py (in make test-offline) fails if a workflow loses its top-level permissions, a checkout persists credentials outside the push allowlist, or the auto-merge guard weakens. The repository's default workflow permissions setting is unchanged and remains a maintainer decision.
  • Tiered CI validation; no duplicate PR live matrices (#222) — the new scripts/ci_scope.py classifies every ci.yml run from its event and changed paths, and the live CUBRID lanes now start only when selected: docs-only pull requests start no live CUBRID; a recipe pull request runs only its family's live lane (the representative Flask/FastAPI 01-basic-crud suites, dashboard, async-worker, Django, CQRS) on CUBRID 11.4 / Python 3.12; golden examples and AI-agent changes run make verify scoped to the touched examples. Shared live infrastructure (Makefile, Compose, normalizer, readiness/coverage scripts) and CI-policy changes fan out to every lane, CUBRID 11.2 + 11.4 and the Python 3.11/3.14 endpoints; changes to fundamentals/pycubrid or fundamentals/sqlalchemy add the endpoints; recipe 10's requirements.txt runs CQRS on both versions. Pushes to main, a new weekly schedule and manual ci.yml runs keep Python 3.11–3.14 and CQRS on 11.2 + 11.4. smoke-test.yml no longer runs on pull requests: the PR golden/AI-agent/MCP smoke moved into two ci.yml jobs (shared steps in .github/actions/pr-smoke) that keep the required check names Smoke Tests (CUBRID 11.2) and Smoke Tests (CUBRID 11.4), so branch protection is unchanged and a skipped lane reports skipped. smoke-test.yml keeps the broad push/nightly 11.2/11.4 run and its exact release verification inputs, outputs and fail-closed reporting; the daily schedule stays because 10 of 37 recent scheduled runs caught make verify failures, mostly on days without a main push. The cubrid-mcp-server tool-contract smoke moved verbatim into scripts/mcp_smoke.py, shared by both workflows. CI Gate (all checks must pass) is a fail-closed allow-list: a lane may be skipped only when the classifier output for it is exactly false; a failure, cancellation, unexpected skip or failed classification fails it. tests/test_ci_scope.py (run by make check) covers the classifier, the gate and the workflow wiring. Measured on real runs and the last 40 merged pull requests: a docs-only PR goes from 21 jobs (12 live) / ~24 billed minutes to 10 jobs (0 live) / ~10, a Dependabot recipe bump to 11 jobs (2 live) / ~11, and the 40-PR total from ~960 to ~734 billed minutes; CONTRIBUTING.md "CI tiers" has the full table and SUPPORT_MATRIX.md now states which tier/event backs each "CI-verified" claim. Dependabot grouping is unchanged.
  • CODEOWNERS routing for high-blast-radius surfaces (#223) — new .github/CODEOWNERS routes review of workflows (including the reusable smoke-test.yml release verification contract), dependabot.yml, SECURITY.md and shared validation/golden infrastructure (Makefile, scripts/, tests/) to the maintainers; recipes are intentionally unowned and the file is routing only, not a security boundary. SECURITY.md gains a durable supported-versions statement and notes that private vulnerability reporting is not enabled (email remains the private route). Repository settings are unchanged.
  • Python 3.11 or later required (#220) — Python 3.10 reached upstream end of life on 2026-10-01, so the cookbook's current support range is now 3.11–3.14: the README badge and prerequisites, SUPPORT_MATRIX.md, CONTRIBUTING.md, llms.txt and recipe READMEs say 3.11+, Ruff targets py311, and Python 3.10 is removed from the required representative compatibility matrix (no jobs were added; real-query/golden checks continue on 3.11–3.14). Migration: Python 3.10 users need an older compatible cookbook/driver combination (earlier cookbook commits and the pycubrid/sqlalchemy-cubrid releases they pin) or must upgrade Python. Standalone recipe requirements.txt files were audited and contain no explicit Python 3.10 constraints; framework and driver floors are unchanged, and teaching examples were not rewritten for 3.11 syntax. Historical entries and measurements are preserved.
  • Recipe dependency floors aligned with the documented support contract (#152) — several standalone requirements.txt files pinned pycubrid well below the documented SUPPORT_MATRIX.md floor (migration/java-to-python, quickstart/5min-fastapi at >=0.5; two FastAPI recipes at >=0.2.6; fundamentals/pandas, fundamentals/sqlalchemy, templates/dashboard, templates/django at >=1.3.2; fundamentals/isolation-levels, alembic, json, async and pitfalls/reserved-words at the stale >=1.6.0), and quickstart/5min-sqlalchemy left both drivers unbounded. Every standalone FastAPI and Flask recipe requirements.txt (templates/api-service-fastapi/recipes/*, templates/flask/*) also listed pycubrid/sqlalchemy-cubrid with no version at all, even though each recipe installs its own requirements.txt independently per its README. All of these now pin the documented floor for their category: the global minimum (pycubrid>=1.6.1, sqlalchemy-cubrid>=1.0), the >=1.4.2 advanced-SQLAlchemy floor, or the >=1.7,<2/>=1.8,<2 per-recipe/per-template exceptions already in SUPPORT_MATRIX.md — which now also documents the templates/ family's >=1.7,<2 convention explicitly. The new scripts/check_dependency_floors.py (run by make check) fails on a bare driver requirement, a floor below its documented minimum, or an undocumented custom pin, so this can't silently drift again; tests/test_dependency_floors.py covers it. CONTRIBUTING documents the policy. Release-smoke VCS/direct-URL contamination is unaffected: it is already handled separately by the existing driver-selection guard (#147/#161, documented in CONTRIBUTING's "Release smoke dependencies").
  • Dependabot version updates configured; workflow actions aligned (#153) — dependabot-auto-merge.yml existed without any .github/dependabot.yml, so no scheduled version updates were ever opened. The new config checks GitHub Actions weekly (one grouped PR) and every standalone recipe requirements.txt monthly, grouped by dependency name so a package bump is one PR across all recipes rather than one per recipe directory; CUBRID driver major updates are ignored because their floors are the SUPPORT_MATRIX.md contract, and versioning-strategy: increase-if-necessary keeps Dependabot from raising a recipe's own floor on a minor/patch bump when the existing range already allows the new version. PR titles and squash commits use the chore(deps): prefix, and the existing auto-merge workflow (patch/minor only, gated on the required checks) is unchanged. ci.yml, smoke-test.yml and docs-sync.yml now use the same actions/checkout v7.0.1 and actions/setup-python v7.0.0 SHA pins as docs.yml (previously v4.4.0/v5.6.0 there). tests/test_dependabot_config.py (run by make check) fails when a recipe requirements.txt falls outside the configured directories, the commit prefix changes, the pip versioning strategy changes, or an action is not SHA-pinned or pinned to different versions across workflows. CONTRIBUTING documents the dependency maintenance policy.
  • Unified issue, pull request and commit titles — CONTRIBUTING.md now defines the cubrid-lab title rule (type(scope)!: description with feat, fix, docs, test, perf, refactor, ci, build, chore, style, revert; no bracket prefixes or issue numbers), and AGENTS.md, the pull request template and both issue forms point to it. The issue forms now prefill fix: (bug report) and feat: (example request) instead of [Bug]: and [Example]:. The new unprivileged PR title workflow (.github/workflows/pr-title.yml) checks every pull request title, and tests/test_pr_title.py (run by make test-offline) exercises its embedded validator.
  • Manual release smoke runs pin the requested release (#177) — smoke-test.yml's workflow_dispatch now takes package (latest, pycubrid, sqlalchemy-cubrid, cubrid-mcp-server) and version (MAJOR.MINOR.PATCH or vMAJOR.MINOR.PATCH) inputs. scripts/release_smoke.py reads them from the event JSON and routes a pinned manual run through the same exact PyPI install, bounded retry, constraint pinning, package-index origin check and job summary as an upstream repository_dispatch, so the upstream RELEASING.md fallback (gh workflow run smoke-test.yml -R cubrid-lab/cubrid-cookbook-python -f package=<package> -f version=<X.Y.Z>) verifies the exact release instead of floating latest. Invalid or unavailable requests fail; a manual run with default inputs keeps the ordinary latest-release smoke behavior and is reported as not a release verification.
  • Release verification runs are identified by request id and report the installed version (#187) — smoke-test.yml accepts an optional request_id (^[A-Za-z0-9._-]{8,80}$) in the repository_dispatch payload and as a workflow_dispatch input, read and validated from the event JSON by scripts/release_smoke.py. A release verification run is named Release verification <package>@<version> [request_id=<id>], so the upstream release workflows (cubrid-lab/pycubrid#539) can find exactly their run through the Actions API, and the request id joins the non-cancelling concurrency group. A new Release verification report job combines both CUBRID jobs into a job summary, step outputs and a release-verification-<request_id> artifact (release-verification.json, schema version 1: requested and installed version, status, reasons, run and per-job details) and fails when a job failed or installed a different version. The contract is documented in CONTRIBUTING; the bounded publication retry and the no-fallback rule are unchanged.
  • Package release workflows can call the smoke test without a token (#190) — smoke-test.yml now also runs on workflow_call, so the release workflows in pycubrid, sqlalchemy-cubrid and cubrid-mcp-server can run it as a job with their own GITHUB_TOKEN (uses: cubrid-lab/cubrid-cookbook-python/.github/workflows/smoke-test.yml@<sha>; inputs package, version, request_id; outputs status, requested_version, installed_version, artifact) instead of a cross-repository dispatch token. Each job detects the call (job.workflow_ref differs from the caller's github.workflow_ref), checks out cubrid-lab/cubrid-cookbook-python at the called workflow's own commit, and scripts/release_smoke.py reads the call inputs from RELEASE_INPUT_* environment variables instead of the caller's event JSON, with the same exact-install, bounded-retry, no-fallback and requested == installed rules. Release verification concurrency groups now also include the run, so a called run never shares or cancels the caller's group. repository_dispatch and workflow_dispatch are unchanged; the call contract is documented in CONTRIBUTING.
  • Release smoke verifications are no longer cancelled by other runs (#177) — smoke-test.yml's concurrency group now includes the event name, requested package and version/ref, and cancel-in-progress is off for release verifications (repository_dispatch, or workflow_dispatch with a non-latest package), so a push to main or a second release no longer cancels an in-flight release check. Push, PR, schedule and unpinned manual runs still cancel superseded runs of the same kind.
  • Removed stale nested Flask recipe copies (#136) — deleted the seven duplicated 01-basic-crud directories from the category templates while retaining the canonical templates/flask/01-basic-crud/ recipe. Thanks @jackgedge.
  • batch-etl quiets Alembic's plugin-discovery logging — templates/batch-etl/04_etl_pipeline.py configures the root logger at INFO. sqlalchemy-cubrid 1.8.0 (#504) imports its Alembic DefaultImpl when the CUBRID dialect loads and Alembic is installed, and Alembic >=1.18 logs setup plugin ... at INFO from alembic.runtime.plugins on import — noise from Alembic's own plugin discovery, not the recipe's ETL output, that only surfaces when Alembic happens to be installed alongside this example (e.g. the full make verify / smoke-test run, which also installs fundamentals/alembic's requirements into the same environment). configure_logging() now sets the alembic logger to WARNING after basicConfig() so the recipe's own INFO lines stay unaffected. The golden output is unchanged; batch-etl is the only template that configures INFO-level root logging via logging.basicConfig, so no other recipe is affected.
  • pycubrid 1.8.0 goldens — fundamentals/pycubrid/16_batch_error_handling now expects no exception-detail suffix: pycubrid 1.8.0 keeps errno on batch errors (#390), so str(exc) ends with (errno=-670, description=..., sqlstate='23000'), which the existing normalizer rule strips (1.7.x printed only (sqlstate='23000')). fundamentals/pycubrid/20_timezone_datetime now expects DATETIMELTZ in the pinned Asia/Seoul session zone (+09:00, same instants): pycubrid 1.8.0 keeps the CAS session across commit() (#468/#472), whereas 1.7.x reconnected on commit and the read ran in a fresh UTC session. The recipe's comments that described UTC-aware LTZ reads as driver behavior are corrected. These goldens assume pycubrid >= 1.8.0, so fundamentals/pycubrid/requirements.txt is raised from pycubrid>=1.7,<2 to pycubrid>=1.8,<2 (documented consistently in its README and SUPPORT_MATRIX.md); fundamentals/connect and fundamentals/orm-basics keep pycubrid>=1.7,<2, since their goldens do not depend on either behavior.
  • Make commands fail closed — make verify rejects missing/empty roots, zero golden targets, orphaned goldens, and discovery/read errors. make up bounds in-container readiness probes and failure diagnostics, preserving containers and data for inspection. Offline command tests use fake recipes and Compose processes, not live database substitutes.
  • FastAPI recipe tests can run against live CUBRID (#122): every templates/api-service-fastapi/recipes/*/tests/ directory now has a conftest.py that provides the database engine fixture. It connects to CUBRID_TEST_URL when set and falls back to in-memory SQLite otherwise, points the recipe's own DATABASE_URL at the same test database, drops and recreates the recipe's tables around each test (so an interrupted run cannot break the next one), and reports which database is in use in the pytest header. The 12 test_main.py files lose their copy-pasted SQLite engine setup, and recipes 11 and 12 no longer force DATABASE_URL to SQLite. All 82 tests pass on CUBRID 11.2, CUBRID 11.4, and the SQLite fallback. The recipe READMEs, the template README, and SUPPORT_MATRIX.md document CUBRID_TEST_URL.
  • Templates now install pycubrid and sqlalchemy-cubrid from PyPI instead of git+…@main — the async-worker, batch-etl, and flask templates' requirements.txt pinned both drivers to the main branch heads, so a template install could break whenever main moved and required a git toolchain at deploy time. They now pin pycubrid>=1.7,<2 and sqlalchemy-cubrid>=1.7,<2 (the current PyPI releases). The api-service-fastapi template's pycubrid>=0.0.4 floor — a pre-release number predating the first public PyPI release — is raised to the same >=1.7,<2 pin.
  • Flask recipe tests can run against live CUBRID (groundwork for #131): each of the 11 templates/flask/*/tests/ directories now has a conftest.py that provides a database_config fixture for create_app(). It uses CUBRID_TEST_URL when set and falls back to a temporary SQLite file per test otherwise, drops the recipe's tables before and after each test (so an interrupted run cannot break the next one), disables connection pooling so the per-test apps do not leave idle connections on the CUBRID broker, puts the recipe directory on sys.path, and reports which database is in use in the pytest header. The test files pass the fixture to create_app() instead of building SQLite URLs. All 89 tests pass on CUBRID 11.2, CUBRID 11.4, and the SQLite fallback. Every recipe README now has test instructions (previously only 01 and 09 did), and SUPPORT_MATRIX.md documents CUBRID_TEST_URL.
  • Refactored to Python-only repository (removed planned Go and Node.js examples)

Fixed

  • The pandas clean-and-transform golden passes on pandas 3.1 (#443) — fundamentals/pandas/03_clean_and_transform.py printed its seven-column result with print(df), which truncates a wide frame to the display width; pandas 3.1.0 changed that truncated layout, so the golden failed on every CI run that installed it. The recipe now prints DataFrame.to_string(), which shows every column and gives the same output on pandas 3.0 and 3.1; the golden is regenerated.
  • Agent-eval tasks can record make verify-goldens as their live check (#440) — scripts/agent_eval.py accepted only make verify ... and pytest commands in validation.live, so a task based on a commit after #437, where the golden check is make verify-goldens VERIFY_PATHS=..., failed validation. run --live also keyed the interpreter override and the golden Results: grading on make verify alone, so such a command would have run without PYTHON= and been graded as a pytest suite. Both now treat make verify-goldens like make verify; existing tasks, which record make verify VERIFY_PATHS=... for their base SHAs, are unchanged.
  • The dashboard and FastAPI API service templates keep their database when Compose recreates the CUBRID container (#413) — the image registers the database in databases.txt for the container's host name, its random ID, and a recreated container (docker compose up -d --force-recreate, or a changed service configuration) inherits the old anonymous volumes but gets a new ID, so every connection failed with -353. Both templates' cubrid services now set a fixed hostname:, as docker-compose.yml and the quickstarts do (#385). The dashboard template, on CUBRID 11.4, also mounted its named volume only at /var/lib/cubrid, which 11.4 does not use, so its database lived in an anonymous volume and down lost it; a second named volume now keeps /home/cubrid/CUBRID/databases, and docker compose down -v removes both. Verified live: both templates keep csql SELECT 1 after a forced recreate, and the dashboard keeps a table across down and up. tests/test_quickstart_config.py pins the host names and the dashboard's volumes.
  • The FastAPI API service template starts with its own Compose file (#412) — templates/api-service-fastapi/docker-compose.yml never reached a healthy database, so docker compose up --build stopped with dependency failed to start: container cubrid-db is unhealthy and the API never started. Its health check grepped cubrid service status for Server is running, which CUBRID 11.2 does not print (Server cubdb (rel 11.2, pid ...)), and the service set no CUBRID_DB, so the image created its default cubdb while .env.example connects to testdb (-677). The service now creates testdb and the health check runs csql -u dba testdb -c "SELECT 1", as the quickstarts do; verified live with cp .env.example .env and docker compose up --build, after which GET /health returned {"status": "ok", "database": "connected"}. tests/test_quickstart_config.py now checks that the template's Compose database, its health check and .env.example agree.
  • The SQL echo example no longer depends on the driver's connection setup queries (#410) — fundamentals/orm-basics/01_engine.py passed echo=True to create_engine(), so its golden recorded the dialect's first-connect queries (SELECT VERSION(), SELECT SCHEMA()). sqlalchemy-cubrid main adds a backslash-mode probe (SELECT CHAR_LENGTH('\\'), sqlalchemy-cubrid #874), which made the advisory driver-main lane fail. The example now opens one connection before setting engine.echo = True, so the log and the golden show only the example's own statements; a comment notes that echo=True at creation also logs the setup queries. Verified with released drivers (pycubrid 1.13.0, sqlalchemy-cubrid 1.11.0) and with both drivers' main against CUBRID 11.4.
  • scripts/run_example.py rejects a non-finite --timeout (#382) — the wall-clock runner behind make verify checked only timeout <= 0, so --timeout nan, inf and an overflowing 1e309 passed as a "deadline" and reached the child process. It now requires a finite value greater than 0 and exits with the usage error needs a finite positive --timeout and a command (status 2) before starting anything. Finite values, including fractions such as 0.25, the timeout exit 124, exit-status propagation and process-group cleanup are unchanged. New tests in tests/test_make_commands.py assert that neither run() nor Popen is reached for these inputs.
  • The async-worker cleanup task rejects a negative retention period (#383) — cleanup_old_records(days) computed now - timedelta(days=days) unchecked, so days=-1 moved the cutoff into the future and deleted recent completed Jobs and Reports, then recorded success. It now requires an int of 0 or more (rejecting bool, floats, strings and None with TypeError, negative values with ValueError, and values too large to compute a date with ValueError), before it records a Job or opens a session. 0 keeps its existing meaning, every completed record created before now, and the default of 30 days and the selection and deletion SQL are unchanged. New tests in templates/async-worker/tests/test_retention.py prove the rejection happens before any database work and, against live CUBRID with a fixed clock, that only completed records strictly older than the cutoff are deleted for 30 and 0 days, and that a rejected call leaves recent records in place. The template README documents the rules.
  • The async-worker example finishes failed and retried job attempts (#384) — each task committed a STARTED Job before its work, but only success, and the last of send_notification's retries, updated it. A caught failure, and every retried send_notification attempt, left a row that looked as if it were still running and that cleanup_old_records never removed. Each Job is now documented as one attempt: a failure after the row exists records FAILED, a send_notification attempt that Celery retries records RETRY, both with completed_at, and the task then re-raises its own error or Celery's retry signal unchanged. aggregate_sales validates its dates before it writes a row. If recording the status fails too, that is logged and the original error still propagates; send_notification records its success outside the retried block, so a failed status update can no longer resend the email. Successful results are unchanged. New live-CUBRID tests in templates/async-worker/tests/test_job_finalization.py cover an invalid date, workload and batch failures, a failed status update, a retry followed by success and exhausted retries, and the template README documents the Job states and the limits (an unavailable status store or a killed worker still leaves STARTED).
  • The local lab keeps a usable database across docker compose down, and listens on localhost only (#385) — two problems made down lose the lab. The 11.4 image keeps its databases in /home/cubrid/CUBRID/databases, which docker-compose.yml did not mount, so down orphaned the database in an anonymous volume. And CUBRID records the host name in databases.txt, so even a kept database (11.2's /var/lib/cubrid) failed to open in the next container, with a new random host name (-353). A second named volume now keeps the 11.4 path, hostname: cubrid-cookbook keeps the host name stable, and docker compose down -v removes both volumes. This was verified on 11.4 and 11.2 with stop/start, down/up and down -v. The published port is now 127.0.0.1:33000, because the lab's dba account has no password. CI starts its own CUBRID containers, so CI is unaffected.
  • A relative link with both a query string and a fragment was staged as a broken link (#361) — scripts/stage_docs.py _split() tested # before ? instead of cutting at whichever delimiter appears first, so a target such as SUPPORT_MATRIX.md?view=raw#versions kept ?view=raw inside its path; the repository existence check then looked for a file literally named SUPPORT_MATRIX.md?view=raw, staging reported a broken link and exited non-zero, and make docs (the strict docs-site build in CI) failed on it. _split() now splits at the first # or ? and keeps the remainder in its original order, so anchor-only links (pitfalls/#top), query-only links and links whose fragment itself contains a ? are rewritten exactly as before, and a missing local source still produces the broken-link error naming the path. tests/test_stage_docs.py covers a staged file, a staged directory and the missing-target case; the module doctest pins the mixed query-plus-fragment form.
  • The collection-types example cleans up after a failed setup (#342) — fundamentals/sqlalchemy/07_collection_types.py started its cleanup try after Base.metadata.drop_all(), create_all() and the first prints, so a setup failure left the engine undisposed and could leave the table behind. The try now starts right after create_engine(), as in the other SQLAlchemy examples; output and golden are unchanged. With create_all() forced to fail, the cleanup drop now runs and the engine is disposed of (neither happened before).
  • Drifted comments, benchmark labels and nondeterministic goldens are corrected (#329) — The READMEs of FastAPI recipes 09–12 used a developer-machine path (cd /data/GitHub/cubrid-cookbook/python/...); they now cd templates/api-service-fastapi/recipes/<recipe> from the repository root. fundamentals/parameterized-queries/README.md said pycubrid has no typed parameter payload; it now says that is the default path and that executemany(..., prepared=True) (pycubrid 1.12.0) sends PREPARE plus EXECUTE_ARRAY with typed binds. Docstrings promised demos the code did not contain: 03_transactions.py points to fundamentals/isolation-levels/ for isolation, 19_pagination.py states its ROWNUM caveat as a note, and fundamentals/async/01_async_pycubrid.py now uses async with for the connection and the cursor (its golden adds one summary line; it also passes on the pycubrid==1.6.1 floor). Benchmarks: performance/fetch-optimization/benchmark.py ran the same 5-column query as SELECT * under a second name; that slot is now a fetchmany(500) loop (49 ms, 47–77, median of 5 on pycubrid 1.12.0; performance/README.md and the recipe README say which row changed), and bulk-insert/benchmark.py says its per-row figure is one INSERT plus its COMMIT. Wrong claims: migration/java-to-python/05_batch_operations.py said a failed single-commit batch rolls back by itself; it now rolls back in an except block. 15_cursor_memory_bound.py concluded that a smaller fetch_size caps memory while measuring fetchall(); it now streams with fetchmany(10) at each fetch_size, prints a fetchall() peak next to them, and its takeaway says peak memory follows the rows kept (the measured fetch_size peaks differ by only a few KB). Unstable goldens: fundamentals/orm-basics/05_dml_extensions.py renames its reserved-word columns (key → config_key, value → val, count → cnt) and orders the initial counters by name; 13_atomic_counters.py passes the recorded day to the ranking instead of reading the clock again, so a run that crosses UTC midnight no longer prints an empty ranking. Cleanup: fundamentals/alembic/alembic_programmatic.py removes its temp working directory, and it, fundamentals/isolation-levels/01_isolation_levels.py and fundamentals/sqlalchemy/07_collection_types.py run their cleanup in a finally block. scripts/mcp_smoke.py no longer calls v0.2.1 the pinned MCP server version. Goldens were regenerated through scripts/normalize_output.sh; all 61 pass twice in a row on live CUBRID 11.4.
  • Leases, chat turns and transfers are checked in SQL (#328) — templates/flask/08-transactional-outbox: /ack and /fail checked the lease in Python and then wrote the message unconditionally, so a lease that expired and was taken again between the read and the write was overwritten (a second processor's lease marked sent, or requeued, and an attempt row added). Both now settle the message with one conditional UPDATE that checks leased_by, leased_until, status and the attempts value they read, return 409 unless exactly one row changed, and only then add the attempt row; the README describes it. templates/ai-agent/05_chatbot_backend.py recorded turn: 0 on every message because len(conv.messages) was read from a relationship loaded once while empty; the turn now comes from the loop, the transcript prints it, and the recipe checks that the turns are 0 to 3. migration/java-to-python/04_transaction.py committed the debit even when the credit updated no rows (an unknown receiver), so money disappeared; the Python code and the Java comparison now check the credit's row count and roll back, and the demo transfers $50 to an unknown receiver (failed (no receiver), balances unchanged). fundamentals/pycubrid/12_pool_retry_worker.py said its claim stopped two workers from taking the same job while its docstring said single-worker, and it read back any processing row; it now picks a queued id, claims it with UPDATE ... WHERE id = ? AND status = 'queued', retries when the row count is 0, and reads the claimed job back by id; a new Claim race section has two connections read the same next job, shows that only the first conditional claim wins (True, then False (rowcount 0)) and that the loser picks the next id. New tests run a re-lease on another connection between the endpoint's read and its write (ack and fail: 409 and the second lease intact; both return 200 on the previous code), and the live AI-agent test checks the chat turns. The 04_transaction and 12_pool_retry_worker goldens were regenerated through scripts/normalize_output.sh.
  • Every example table is prefixed and has one schema, and the guard scans all example files (#327) — templates/ai-agent created unprefixed tables (agent_sessions, agent_messages, agent_tool_calls, rag_documents, rag_chunks, rag_retrieval_log, chat_users, chat_conversations, chat_messages); they are now cookbook_agent_*, cookbook_rag_* and cookbook_chat_*. Thirteen cookbook_ names were created by several directories with different columns, so running those examples against one database collided (cookbook_sales had three schemas, and the dashboard's FK to cookbook_products could block another example's DROP). One owner keeps each name and the others are renamed: cookbook_sales stays with batch-etl (dashboard → cookbook_dashboard_sales, fundamentals/pycubrid/11 → cookbook_pipeline_sales, 17 → cookbook_window_sales); cookbook_products stays with Flask recipe 01 (dashboard → cookbook_dashboard_products, fundamentals/pandas/01 → cookbook_pandas_products, fundamentals/pycubrid/04 → cookbook_prepared_products, 07 → cookbook_merge_products, fundamentals/sqlalchemy/04 → cookbook_bulk_products, migration/java-to-python/03 → cookbook_jdbc_products); cookbook_items stays with the FastAPI template app (FastAPI quickstart → cookbook_quickstart_items, Django → cookbook_django_items, migration/java-to-python/02 → cookbook_jdbc_items); cookbook_orders with FastAPI recipe 02 (fundamentals/pandas/02 → cookbook_pandas_orders, fundamentals/pycubrid/05 → cookbook_error_orders, 09 → cookbook_serial_orders); cookbook_users with Flask recipe 09 (fundamentals/pycubrid/02 → cookbook_crud_users, fundamentals/sqlalchemy/02 → cookbook_orm_users); cookbook_documents with FastAPI recipe 07 (fundamentals/pycubrid/06 → cookbook_lob_documents and cookbook_lob_files); cookbook_jobs with async-worker (fundamentals/pycubrid/12 → cookbook_retry_jobs); cookbook_tasks with FastAPI recipe 01 (fundamentals/pycubrid/14 → cookbook_cascade_accounts, _projects, _tasks, _comments); cookbook_accounts with fundamentals/pycubrid/03 (migration/java-to-python/04 → cookbook_jdbc_accounts); cookbook_counters with fundamentals/pycubrid/13 (fundamentals/orm-basics/05 → cookbook_dml_counters); cookbook_departments/cookbook_employees with fundamentals/sqlalchemy/03 (fundamentals/orm-basics/02 → cookbook_core_*, fundamentals/pycubrid/18 → cookbook_cte_employees); and cookbook_ref_authors with fundamentals/sqlalchemy/06 (fundamentals/orm-basics/06 → cookbook_reflect_authors/cookbook_reflect_articles, listing tables by the cookbook_reflect_ prefix). tests/test_table_prefix.py used to read only templates/**/models.py; it now parses every *.py under the example roots outside tests/ directories (ORM __tablename__ with its mapped columns, Table(...), pandas to_sql(...), and raw CREATE TABLE text including f-strings over module-level constants) and fails when a table lacks the prefix, when two directories declare the same name, or when one name is declared with two column sets (pitfalls/reserved-words/01_reserved_words.py is exempt with a reason: it creates one table with rejected, corrected and quoted columns). It fails on the previous code and passes now. The renamed goldens were regenerated through scripts/normalize_output.sh; all 61 goldens, the AI-agent suite (twice), the Django and dashboard suites pass on live CUBRID 11.4. SUPPORT_MATRIX.md, the affected READMEs, the demo recording commands and the CI workflow comments use the new names; no workflow logic changed.
  • Fundamentals examples, READMEs and goldens match the pycubrid 1.12 APIs (#325) — fundamentals/json/01_json_crud.py passed '"light"' to JSON_REPLACE, which stores the quotes as part of the JSON string ("\"light\"", confirmed in csql), and its golden recorded '"light"'; it now binds 'light' as a ? parameter, prints 'light', and the cheat sheet notes that a SQL string becomes a JSON string. fundamentals/pycubrid/README.md documented conn.create_lob("CLOB", data), which raises TypeError (the signature is create_lob(lob_type: int), and execute() and the default executemany() cannot bind the returned Lob, ProgrammingError: unsupported parameter type; only executemany(..., prepared=True) with autocommit off binds a same-connection Lob, since pycubrid 1.12.0); the LOB section now shows what 06_lob.py does (bind str/bytes, read back through pycubrid.lob.Lob, checked live), the API table gives the real signature, and the 01_connect.py sample output matches its golden (val, current type codes, placeholders for the server version, host name and date, the multiple-queries block); llms.txt no longer points at create_lob(). 20_timezone_datetime.py reads TIMESTAMPTZ natively next to DATETIMETZ/DATETIMELTZ (pycubrid#289 was fixed in 1.7.0, below the directory's pycubrid>=1.9 floor) and keeps TO_CHAR(... TZR) for the zone region name. 16_batch_error_handling.py described the executemany_batch return value as (result_code, affected_count); it is (statement_type, result_count), with the statement type a CUBRIDStatementType value. Nine files annotated with pycubrid.Connection/pycubrid.Cursor, which pycubrid does not export (it worked only because from __future__ import annotations never evaluates them); they import Connection from pycubrid.connection and Cursor from pycubrid.cursor. The three affected goldens were regenerated through scripts/normalize_output.sh and pass live on CUBRID 11.4.
  • The CQRS rebuild replays every event into the read model (#326) — in templates/api-service-fastapi/recipes/10-cqrs-event-sourcing, the projection skipped every state with sequence_no <= last_sequence_no, which also applied to /rebuild, so even {"from_scratch": 1} left a drifted read model unchanged (balance_cents stayed at 1 instead of 4000). _project_read_model() takes a keyword-only replace flag that /rebuild sets, so both rebuild modes overwrite the row with the replayed state; command projections keep skipping states they already reached. The projection UPDATE keeps its version guard and adds last_sequence_no <= :sequence_no, so a rebuild whose replay was overtaken by a newly committed command returns 409 instead of rewinding the read model. A drift-then-rebuild test for both modes fails on the previous code and passes on SQLite and live CUBRID 11.4, and a race test commits a deposit between the replay and the write; the recipe README explains the difference.
  • The rate-limiter recipe stores a rotated window's count and checks its reset version in SQL (#323) — in templates/api-service-fastapi/recipes/11-rate-limiter, a window rotation set current_count = 0 only on the Python object (the session uses autoflush=False), and the following UPDATE current_count = current_count + cost added to the old database value: consume 4, wait past the window, consume 1, and the stored count was 5 instead of 1. The first consume of a new window now writes current_count = cost. /reset compared expected_version in Python and then let the ORM flush an unconditional UPDATE, so a consume that committed between the read and the write was lost; it is now a conditional UPDATE with version = expected_version in its WHERE clause and returns 409 unless exactly one row changed. Two new tests fail on the previous code and pass on SQLite and live CUBRID 11.4; the recipe README describes both checks.
  • Saga recovery no longer repeats completed steps (#322) — in templates/api-service-fastapi/recipes/09-saga, /recover resets a stuck order to pending, but /execute then ran every step again: after a crash following reserve_inventory, recover plus execute reserved the stock twice (reserved_qty 6 instead of 3) and charged the payment twice. /execute now reads which steps are already done (each step's effect commits in the same transaction as its done status) and skips them, and a later failure still compensates the skipped steps. New tests leave the database as a run that crashed after the reservation, or after the charge, then recover and execute: stock and payment change exactly once, and a failing payment releases the reservation made before the crash. They fail on the previous code and pass on SQLite and live CUBRID 11.4. The recipe README documents /recover and the resume behavior.
  • The SQLAlchemy quickstart connects to the database its Compose file creates (#324) — quickstart/5min-sqlalchemy/docker-compose.yml created demodb while app.py connected to testdb, so following the README failed at connect. The Compose file and its health check now create and probe testdb, the README starts it with docker compose up -d --wait and ends with docker compose down, and app.py drops each of its two cookbook_ tables on its own and disposes of the engine in a finally block, so a run leaves nothing behind even after a failure. The new tests/test_quickstart_config.py (in make test-offline) fails when any quickstart's CUBRID_DB, health-check database and app connection drift apart. The app's output and its golden are unchanged.
  • Renaming README.md no longer skips the Korean translation requirement (#319) — scripts/check_translation_sync.py and the changed-file list in docs-sync.yml ran git diff --name-only, which prints only the new path of a rename, so renaming a source file hid the old path (README.md, or an example directory) from the check. Both now pass --no-renames, so a rename is reported as the old path deleted and the new path added and both are listed; a copy never listed its unchanged source, and still lists only the new path. tests/test_translation_sync.py runs the real git diff in a scratch repository for a modification, a rename, a rename with edits and a copy; tests/test_docs_reason.py pins the flag in the workflow.
  • AI agent recipes run again on the same database (#302) — templates/ai-agent/01_agent_state.py and 04_agent_loop.py raised IntegrityError (errno -670) on a second run because their fixed session_key (demo-001, agent-loop-demo) is UNIQUE and the tables are kept; 05_chatbot_backend.py failed the same way on the alice username, and 03_rag_metadata.py re-ingested its three documents on every run, so retrieval returned duplicates. The demonstration identifiers are now recipe-owned (cookbook-agent-state-demo, cookbook-agent-loop-demo, cookbook_demo_alice, and a "recipe": "cookbook-ai-agent-rag-demo" metadata marker on the sample documents), and each recipe first deletes only its own rows, children before parents: 01 adds delete_session() (tool calls, messages, then the session for one key), which 04 reuses; 03 adds remove_demo_documents() (chunks, then the marked documents); 05 deletes its demo user's messages, conversations and user. A reader's rows with a generic key, the username alice or a sample source URL are not touched, and rag_retrieval_log still accumulates. The printed output is unchanged apart from auto-increment IDs and the demo username. The new live test test_ai_agent_recipes_rerun_on_same_database runs each recipe twice without resetting tables (four of the five failed before this change) and checks that such look-alike rows survive the rerun; offline tests check the deletion order and that 01 and 04 delete before they create.
  • Alembic recipe reports transactional_ddl from the dialect; DDL rollback docs corrected (#234) — fundamentals/alembic/alembic_programmatic.py printed a fixed transactional_ddl = False -> each DDL statement auto-commits. and its golden locked that in, although sqlalchemy-cubrid has set CubridImpl.transactional_ddl = True since 1.8.0 (sqlalchemy-cubrid#503). The recipe now prints the impl class name and transactional_ddl read from a configured Alembic MigrationContext at runtime, so the golden fails if the dialect value ever changes. Its generated env.py no longer imports sqlalchemy_cubrid.alembic_impl (with an outdated comment about the removed alembic.ddl entry point): sqlalchemy-cubrid registers CubridImpl automatically since 1.8.0 (sqlalchemy-cubrid#504), so the recipe now shows a default env.py working. The closing pointer to a nonexistent README.md now links sqlalchemy-cubrid's docs/ALEMBIC.md, and the docstring and llms.txt were updated. fundamentals/alembic/requirements.txt raises its floor to sqlalchemy-cubrid>=1.8 (1.7.1 fails with KeyError: 'cubrid' without the manual import); scripts/check_dependency_floors.py guards it as a documented EXACT_SQLALCHEMY_FLOOR exception, including the stale-exception check, and SUPPORT_MATRIX.md documents it. KNOWN_ISSUES.md §3 ("DDL Auto-Commits": migrations not transactional, DDL cannot be rolled back) is rewritten: CUBRID DDL is transactional while autocommit is off (pycubrid's default), ROLLBACK undoes it, schema locks are held until commit, and client autocommit (csql's default, conn.autocommit = True) commits every statement. The SUPPORT_MATRIX.md limitations row is replaced to match. Verified live on CUBRID 11.2.9 and 11.4.6.
  • Empty emphasized link captions no longer exempt documentation updates (#213) — synchronize scripts/check_docs_reason.py with the reviewed pycubrid#573 helper so emphasis around an empty comment or image is not counted as a visible explanation. Regression tests retain all four pycubrid#429 case families and a populated emphasized-reason control; existing cookbook event-JSON tests and maintainer-label exceptions are unchanged.
  • Release dispatch smoke tests verify the requested artifact (#148) — validate upstream package names and canonical release refs from event JSON, install the exact PyPI version with bounded publication retry, and pin/check it through all later dependency installs. Requested MCP releases use the PyPI artifact; its existing git-tag fallback remains only for non-requested MCP. Always report requested and installed versions, origin, verification commit, actual CUBRID server version when available, and result, including failed runs.
  • AI-agent smoke validation follows release verification (#147) — after the AI examples joined PR smoke coverage, their pytest dependency is installed under the frozen release constraints before the final version/provenance check. Both live repetitions run after that check; no dependency install can replace a driver after the tested-version report. Added guards for missing constraints, optional requirements, and same-version local replacements.
  • Release smoke tests preserve and report the drivers actually exercised (#147) — removed four git+main driver references from the pycubrid, connect, and orm-basics fundamentals requirements in favor of >=1.7,<2. Exact constraints now protect the selected published drivers during every later install. The workflow checks versions and package-index origin after all example/framework dependencies are installed, then records the tested versions; conflicting requirements and same-version VCS/local replacements fail. Added offline regression guards, while preserving the intentional MCP tag fallback. The separate upstream tag/PyPI publication race remains unresolved.
  • The host-run FastAPI quickstart could not connect (#146) — default CUBRID_HOST to localhost, and set it to cubrid only for the Compose API service. The host instructions start only the database and wait for its health check, leaving port 8000 for Uvicorn. Replace deprecated startup events with lifespan startup and ensure startup and request database operations commit on success, roll back on failure, and close cursors and connections. Add offline HTTP/configuration/failure regression tests to PR CI.
  • AI agent examples now run against released drivers (#125) — capture cursor.lastrowid before commit, bind SET/SEQUENCE members as scalar placeholders, and read collections via TABLE() instead of raw driver payloads. The agent-loop example creates its own sample products table. Complete and validate MCP initialization/tool responses so connection and protocol errors fail the demo. Close recipe connections and dispose the ORM engine on errors. The five live scripts now have bounded process isolation and per-example table cleanup, and run twice on both CUBRID smoke-test servers, including pull requests; offline tests cover ID handling and error cleanup.
  • The FastAPI template and nine of its recipes could not create their tables on CUBRID (#122): templates/api-service-fastapi/app/models.py and recipes 01-08 declared 20 primary key columns with index=True, and recipe 11 indexed client_rate_windows.client_id on top of its UniqueConstraint. CUBRID rejects a second index over already-indexed columns (Index "pk_cookbook_tasks_id" already defined), so Base.metadata.create_all() failed at app startup on both 11.2 and 11.4. SQLite accepts the redundant index, which is why the SQLite-only test suites never caught it. Removed the redundant indexes and documented the behavior as KNOWN_ISSUES.md #5 and in the SUPPORT_MATRIX.md limitations table.
  • FastAPI recipes 02, 04, and 05 were missing email-validator (#122): their schemas use pydantic's EmailStr, which requires email-validator, but it was not in their requirements.txt, so the apps and their tests failed on import after a clean install. Added it as email-validator>=2.
  • The documented FastAPI test command could not import recipes 01-08 (#122): the SUPPORT_MATRIX.md loop (for d in */tests; do python3 -m pytest "$d" -q; done) failed with ModuleNotFoundError: No module named 'database' for recipes 01-08 because nothing put the recipe directory on sys.path. The new conftest.py does, so the loop runs all 12 suites. The 01-08 README test instructions now also install pytest pytest-asyncio httpx, which their requirements.txt files do not include.
  • Regenerated the two LOB goldens that recorded pycubrid's pre-1.7.0 escaping bug — fundamentals/pycubrid/expected/06_lob.expected and fundamentals/lob-handling/expected/06_lob.expected were captured while pycubrid silently negotiated the wrong no_backslash_escapes mode (the bug fixed loud in pycubrid 1.7.0, pycubrid#263/#293), so every newline in the round-tripped CLOB text carried a stray backslash (64 chars → 66 chars). pycubrid 1.7.0 fixed the corruption, which made the nightly smoke red against these stale goldens since Sep 4. Goldens regenerated from the now-correct output against CUBRID 11.2; make verify passes 23/23.
  • Flask recipes 06, 08, and 11 crashed comparing naive and aware datetimes: the datetime.utcnow() replacement for #30 (#34) switched these apps to datetime.now(timezone.utc), but their DateTime columns store naive UTC values, so comparing a value read back from the database with the new aware "now" raised TypeError: can't compare offset-naive and offset-aware datetimes. Claiming an already-claimed case (06), leasing outbox messages (08), and confirming or sweeping reservations (11) returned HTTP 500, and 11 of their tests failed; it went unnoticed because the Flask suites do not run in CI. The apps now use a naive_utc_now() helper for these comparisons. Writes of aware UTC values elsewhere (recipes 04, 05, 07) were checked on CUBRID 11.2 and 11.4 and store correctly, so they are unchanged.
  • Flask recipe 09 could not start from its own requirements.txt: it did not list pycubrid and sqlalchemy-cubrid, so create_app() failed with Can't load plugin: sqlalchemy.dialects:cubrid.pycubrid. Added both with the template's >=1.7,<2 pins.
  • The documented Flask test command could not import recipe 11: its test module imported app without putting the recipe directory on sys.path, so the SUPPORT_MATRIX.md loop failed with ModuleNotFoundError: No module named 'app'. The new conftest.py adds it.
  • De-duplicated the parameterized-queries recipe golden (#79) — fundamentals/parameterized-queries/04_parameterized.py was a byte-for-byte copy of the canonical fundamentals/pycubrid/04_prepared.py, and both shipped expected/ goldens, so make verify counted the same recipe twice. The topic entry is now a thin redirect to the canonical recipe (its expected/ golden removed and the folder allowlisted in scripts/docs-sync-allowlist.txt), dropping the CI golden count from 46 to 45 while keeping topic-based discovery intact.
  • Fixed 3 broken examples the missing-golden gap was hiding (#89) — backfilled expected/ goldens for 11 previously-unverified scripts and, in doing so, uncovered and fixed three broken examples: fundamentals/error-handling/03_query_timeout.py (a server-side lock_timeout system parameter did not bound the blocked client on CUBRID 11.2, so the demo hung forever — rewritten to use a client-side pycubrid.connect(..., read_timeout=…)); fundamentals/pycubrid/12_pool_retry_worker.py (hung at cleanup because a pooled connection still held a lock when DROP TABLE ran — the finally now closes the pool before dropping); and fundamentals/pycubrid/16_batch_error_handling.py (crashed on a duplicate-key retry that assumed post-failure statements were skipped — rewritten to teach the real CUBRID all-or-nothing rollback() semantics, and a module-level print that scrambled output order was moved into main()).
  • Added datetime-microsecond and bulk-insert timing rules to scripts/normalize_output.sh — {{DATE}} HH:MM:SS.ffffff now normalizes to {{DATETIME}} and the execute(insert, rows):/add_all: perf-summary lines normalize their seconds to {{TIME}}s, so the merge/serial/bulk-insert examples produce reproducible goldens. Covered by new scripts/test_normalize_output.sh cases.
  • 07_merge_upsert output was flaky when a timestamp landed on a whole second (#189) — fundamentals/pycubrid/07_merge_upsert.py printed the last_seen_utc DATETIME column with Python's default datetime.__str__(), which omits the fractional part entirely whenever the microsecond component is exactly 0 (about 1 run in 1000, since CUBRID stores millisecond precision), so scripts/normalize_output.sh's {{DATE}} HH:MM:SS.ffffff rule (which requires a fractional part) didn't match and the golden comparison failed. The recipe now has a format_timestamp() helper that always renders the fractional part (strftime("%Y-%m-%d %H:%M:%S.%f"), truncated to milliseconds), so the existing normalizer rule always matches; widening the normalizer rule itself was considered and rejected; it would have also swallowed the fixed, meaningful whole-second timestamps fundamentals/pycubrid/20_timezone_datetime.py's golden depends on. New scripts/test_normalize_output.sh cases cover a zero-padded-milliseconds whole-second value (now normalizes) and guard that a bare whole-second value with no fractional part still does not (it can be meaningful, as in 20_timezone_datetime). The golden is unchanged (it already compares against the {{DATETIME}} token).
  • Python lint errors and code formatting across all examples
  • All examples verified against live CUBRID instance
  • fundamentals/sqlalchemy/07_collection_types.py — SET/MULTISET/SEQUENCE collection columns now render correct single-quoted SQL literals (with quote escaping) instead of malformed inline SQL, and the example is verified against a golden expected/07_collection_types.expected output (Closes #56)

Documentation

  • README and docs site show the transparent brand icon (#533) — the icon URL pinned in the README, its translations and mkdocs.yml (logo and favicon) now points at brand revision e55990a, where the Cookbook icon's open book is two transparent pages instead of white pages with text lines. Documentation only; behavior is unchanged.

  • README and docs site show the brand icon with white marks (#507) — the icon URL pinned in the README, its translations and mkdocs.yml (logo and favicon) now points at brand revision 3a385be, where the Python Yellow icons draw white role marks on a bare cube like every other language. Documentation only; behavior is unchanged.

  • README and docs site show the emblem-aligned brand icon (#505) — the icon URL pinned in the README, its translations and mkdocs.yml (logo and favicon) now points at the brand revision 538bad3, whose Python Yellow keyline keeps the cube silhouette of the organization emblem (open seams, no flat bottom). Documentation only; behavior is unchanged.
  • README and docs site show the 2026-10-25 brand icon (#499) — the icon URL pinned in the README, its translations and mkdocs.yml (logo and favicon) now points at the approved CUBRID Lab brand revision 8aa81b7. Documentation only; behavior is unchanged.

  • The README and the documentation site pin the approved 2026-10-25 Cookbook icon (#497) — README.md, docs/README.ko.md and the site logo and favicon in mkdocs.yml loaded the icon from an older cubrid-lab/brand commit that predates the brand icon fixes (glyphs kept off the cube seams, redrawn Cookbook mark). They now pin brand commit 2de5d23, the revision recorded as approved in cubrid-lab/brand#6.

  • make test then make verify come first; the focused lanes are advanced (#459) — README.md, GETTING_STARTED.md, CONTRIBUTING.md and make help (with docs/README.ko.md and docs/ko/getting-started.md) now present make test (offline, results in test-results/test/) and make verify (complete live validation) as the standard sequence, and move verify-goldens, verify-quickstart, verify-performance, verify-web, verify-django-celery, verify-ai-agent, verify-dashboard, verify-templates and demo under "Advanced debugging lanes", a separate make help group that says each prints PARTIAL. make check test stays the documented pre-PR command. No target runs anything different; tests/test_make_commands.py pins the make help grouping.
  • One beginner route with five lessons, and reader-based navigation (#392) — steps 4 and 5 of the beginner tutorial are now lessons of their own, in English and Korean: quickstart/first-cubrid/save-a-row.md (lesson 3), rollback.md (lesson 4) and finish.md (lesson 5), staged as first-cubrid-save-a-row, first-cubrid-rollback and first-cubrid-finish. Each states what the earlier lessons produced and has the command, the checked output, what to do when it is run again or interrupted, and the next step. The scripts and their goldens are unchanged, and the old step headings stay as short landings. The site navigation now has Start here (the five lessons), Optional goals (the FastAPI and SQLAlchemy quickstarts), How-to guides (Getting Started as "Choose a path and verify a checkout", the recorded walkthroughs and the recipe pages), Reference, Help (known issues, the issue forms and SECURITY.md), a peer 한국어 group with the same order, and Project. docs/quickstart.md becomes a compatibility page whose old headings point to the tutorial or to Getting Started, and keeps the walkthroughs; the site home links Getting Started for the three paths; GETTING_STARTED.md and its Korean page point newcomers to the tutorial. CONTRIBUTING.md completes the page map and adds a short convention for writing a tutorial lesson.

  • The first query is its own lesson (#392) — quickstart/first-cubrid/first-query.md ("Lesson 2: Run your first query", Korean docs/ko/first-cubrid-first-query.md) is the first lesson split from the beginner tutorial: what lesson 1 must have produced, the unchanged 01_first_query.py, the command, the checked output, what to do when the lab is not ready, what the connection and cursor did, and the next step. Step 3 of quickstart/first-cubrid/README.md is now a short landing that links to the lesson, the tutorial lists its lessons in order, and every heading stays, so old links still land. CONTRIBUTING.md gains a Documentation structure map (source, site page, reader, question, decision and neighbours for every affected page). The lesson follows the tutorial in the site navigation, and pages now show previous and next links.

  • SUPPORT_MATRIX.md separates the support policy, published package requirements and dated test records (#393) — beside the current guidance it still said the sqlalchemy-cubrid <2.2 SQLAlchemy cap was on unreleased main and unpublished, although 1.11.0 had shipped it. The page now has a current support policy, a "Published package requirements" table read from each published wheel's METADATA with its SHA-256 checked against PyPI (sqlalchemy-cubrid 1.11.0 declares sqlalchemy>=2.0,<2.2, 1.10.0 sqlalchemy>=2.0,<2.3, checked 2026-10-16), and the 2026-10-11 observation kept as a dated record; the Python table's resolved-versions column is labelled a dated record with its run, source commit, origin and scope. scripts/check_support_matrix_counts.py (in make check-docs) now rejects a published-requirement row without wheel and SHA-256 evidence, or one for a main, candidate or unverified version, and CONTRIBUTING.md ("Refreshing version statements") names the source of each statement and the documentation maintainer's refresh steps.
  • The demo-recording recipe removes its database volume (#394) — demos/README.md removed the throwaway demo-cubrid container with docker rm -f, which leaves the anonymous volume the cubrid/cubrid:11.4 image declares for /home/cubrid/CUBRID/databases; every recording run left one behind. It now uses docker rm -f -v demo-cubrid, with a comment on what -v deletes.
  • The FastAPI and SQLAlchemy quickstarts are self-contained (#386) — each README now works when opened directly: the assumed OS and shell, the tools and whether host Python is used; git clone, the exact folder, and a venv where one is needed; the observed healthy docker compose ps state; the real output of a create-then-read through the FastAPI API (POST /items then GET /items/1, with the id marked as variable on later runs because the table persists) and of the SQLAlchemy script (matching its golden, ids 1 on every run because it recreates its tables); stopping, rerunning, dropping only the quickstart's own table, and stop versus down versus down -v; the shared port 33000 and Compose project names when switching between the quickstarts and the root lab; and short troubleshooting with the real messages for a missing Docker, an unhealthy database, an occupied port, an inactive venv and a missing or different database. FastAPI keeps two labelled ways, everything in Docker (recommended) and the API on the host with Uvicorn, never both on port 8000, and explains localhost versus the Compose service name cubrid where each applies. "5 minutes" is qualified as an estimate once Docker (and Python) are installed. Both pages are now published on the site (Getting Started), their next steps are links that work on GitHub and the site, and the Korean learning pages are docs/ko/quickstart-fastapi.md and docs/ko/quickstart-sqlalchemy.md. The root README.md and docs/README.ko.md quickstart blocks start from git clone, and llms.txt describes both quickstarts. tests/test_quickstart_config.py fails if the SQLAlchemy README or its Korean page stops showing the golden output.
  • A guided first tutorial for readers new to CUBRID (#385) — quickstart/first-cubrid/ is the new "New to CUBRID? Start here" entry point, ahead of the FastAPI and SQLAlchemy quickstarts in README.md, the site navigation, the site home page and llms.txt. In order, it explains the database server versus the pycubrid connector and the host, port, database and user; prepares the local lab (clone, working directory, venv, docker compose up -d with the readiness script); and runs four small scripts with their real output: a first query; saving rows with ? placeholders, committing them and reading them from a second connection; rollback; and a cleanup that drops only the tutorial's own table. It then covers interrupted runs, re-running, and the difference between stop, down and down -v. The scripts carry make verify goldens, so CI keeps the published output current; a table that is not the tutorial's stops step 4 and cleanup without changes. The Korean learning path is docs/ko/first-cubrid.md, linked from the Korean README.
  • Agent PR review route: Copilot or a maintainer (#351) — maintainer decision of 2026-10-15: a standalone-agent PR needs a substantive submitted Copilot review or a review by a maintainer (a code owner other than the PR's author), recorded on the PR. Reviews by other people or by other AI agents do not satisfy the route. No CI check enforces it; whether to add one is pending (#351). The two-round AI review budget stays. New: under every route, changes to workflows that hold write permissions or that sibling repositories call to verify a release (smoke-test.yml), and to scripts/release_smoke.py and scripts/mcp_smoke.py, need a review that explicitly assesses them, confirmed by a maintainer. AGENTS.md, CONTRIBUTING.md and the PR template are updated; the Korean docs carry no review rules.
  • The README and the documentation site show the CUBRID Lab repository icon (#349) — README.md and the Korean docs/README.ko.md open with the cubrid-cookbook-python Cookbook icon, centered and without a caption, and the documentation site uses it as its header logo and favicon. docs/catalog.md is staged from the README and follows. All of them load the SVG from one immutable cubrid-lab/brand commit.
  • Performance READMEs say how to run their benchmarks — performance/README.md and the fetch-optimization, bulk-insert and connection-pooling READMEs now give the exact commands (CUBRID_VERSION=11.4 docker compose up -d --wait cubrid, pip install of the driver, python performance/<section>/benchmark.py, docker compose down) and the rough run time of each script. The commands were run against cubrid/cubrid:11.4. No script or measured figure changed.
  • The demo recording steps no longer say the agent recipes need an empty database (#310) — demos/README.md still said that 01_agent_state.py and 04_agent_loop.py need an empty database for each run and that the recipes reset their tables, which stopped being true when the AI agent recipes became rerunnable (#302). It now says that the recipes delete only their own previous demonstration rows, and the comment on the DROP TABLE between the two VHS tapes says the drop is optional and only keeps the printed IDs starting at 1, as in the committed recordings. The recordings and recording-provenance.json are unchanged.
  • README demo GIFs no longer start moving on their own (#308) — README.md and the Korean docs/README.ko.md embedded three autoplaying, looping GIFs (docs/demo-agent-state.gif, demo-fastapi.gif, demo-dashboard.gif, 9–12 s each) with no way to pause, stop or hide them (WCAG 2.2 success criterion 2.2.2). Each embed is now a still poster (docs/demo-*.png, the GIF's last frame, 49–99 KB) that links to its GIF, with a "Play the recording (animated GIF, N seconds)" link under it, so the animation plays only when the reader selects it; the GIFs themselves are unchanged. docs/catalog.md is staged from the README and follows. demos/build_media.sh writes the posters from the GIFs, demos/README.md lists them and explains why, demos/recording-provenance.json records their hashes and sizes (and the script's new hash), and THIRD_PARTY_LICENSES.md names them with the recordings.
  • The dashboard table viewer is described as refreshing on a button, not automatically (#304) — templates/dashboard/README.md, README.ko.md and the 01_table_viewer.py module docstring said the table viewer auto-refreshes, but the app re-reads the table only when the reader clicks its Refresh button, and its own caption says "no auto-refresh". The three texts now describe the Refresh button. Auto-refresh was not added: the caption shows the manual refresh is deliberate, the dashboard demo recording clicks Refresh, and Streamlit's timed reruns (run_every on a fragment) need Streamlit 1.33 or later (st.fragment is stable since 1.37) while the template supports streamlit>=1.30. docs/template-dashboard.md is staged from the README and follows.
  • The agent loop recipe no longer claims to run through the MCP whitelist (#305) — the module docstring of templates/ai-agent/04_agent_loop.py said step 3 "Executes against CUBRID via the MCP read-only whitelist", and the template README's architecture diagram labelled the act step "Act (MCP) (read-only)", but SimpleDataAgent.act() runs the planned tool directly with pycubrid and never calls cubrid-mcp-server, so a non-SELECT statement passed to execute_query would not be rejected. The docstring, the README's recipe table and diagram now say that 04 uses MCP-named tools executed directly, and a note after the diagram points to 02_mcp_toolchain.py as the example that goes through the MCP server and its read-only whitelist. The docstring and the README also warn that, once an LLM writes the SQL, execute_query under the default dba connection can modify or drop data, and recommend the MCP read-only path or read-only validation plus a least-privilege user before connecting one. The recipe's code is unchanged.
  • Real demo recordings for the README and the docs site (#113) — docs/demo-agent-state.gif was drawn by demos/render_gif.py from scripted text in demos/agent-state.json, which no longer matched the recipe (it showed 4 messages; 01_agent_state.py prints 3). It is now a VHS recording of the recipe running against CUBRID 11.4, and the renderer and its JSON script are removed. New recordings, all of real runs: docs/demo-fastapi.gif (the 5-minute FastAPI quickstart's Swagger UI creating and listing an item on CUBRID 11.2) and docs/demo-dashboard.gif (the Streamlit dashboard template) in the README and the Korean README, and three docs-site videos embedded in docs/quickstart.md under docs/assets/videos/: dashboard.mp4 (docker compose up -d, then the browser), agent-state.mp4 (01_agent_state.py and 04_agent_loop.py) and mcp-toolchain.mp4 (02_mcp_toolchain.py driving cubrid-mcp-server). Sources live in demos/: four VHS tapes (VHS 0.12.1 or later; 0.12.0 silently writes no file), record_browser.py (Playwright), build_media.sh (ffmpeg excerpts and joins), a README with the steps and what is cut (only the waits for the CUBRID health check and the dashboard's pip install), and recording-provenance.json with package versions, image digests, tool versions and media hashes. The DEMO license-inventory set now covers demos/record_browser.py (playwright, with greenlet, pyee and typing_extensions) instead of the renderer (imageio, pillow); THIRD_PARTY_LICENSES.md was regenerated with the recorded command and --exclude-newer timestamp, which changed only that set, and describes the recordings. GIFs are 8–12 s and at most 1.8 MB; videos are 31–42 s H.264.
  • Korean README for the Flask template (#137) — adds templates/flask/README.ko.md, a Korean translation of templates/flask/README.md (requirements, recipe layout, running a recipe, tests and further detail), following the header and terminology of templates/dashboard/README.ko.md. The English README stays canonical.
  • Project Structure section in the Korean README (#248) — docs/README.ko.md gains ## 프로젝트 구조, a translation of the English README's Project Structure tree, placed before 관련 프로젝트 as in README.md. Directory names and counts match the English tree; only the comments are translated.
  • ROADMAP describes the Python-only cookbook (#155) — ROADMAP.md no longer plans Rust examples or claims TypeScript, Go and Rust coverage, and drops the Drizzle/GORM framework list. Its blanket "all examples tested against CUBRID 11.2" line now points to SUPPORT_MATRIX.md, which records which CUBRID and Python versions and CI tiers verify each recipe, and the framework list points to the README instead of being restated. The bug report form drops its Node.js and Go example options and asks for the Python version only. The duplicate [Unreleased] headings #155 also reported were already consolidated on main and are enforced by scripts/lint_changelog.py.
  • Resolved driver versions recorded for the Python endpoint cells (#218) — the Python Versions table in SUPPORT_MATRIX.md gains a dated column with the pycubrid, sqlalchemy-cubrid and SQLAlchemy versions the Python 3.11–3.14 compatibility jobs resolved (ci.yml run 38125283280). Documentation only.
  • main branch protection baseline and security contact route (#223) — CONTRIBUTING.md documents the main ruleset (pull request required, CI Gate (all checks must pass) and PR title as the only required checks, no deletion or force push, no required approval and no bypass actor) and states that outside contributors need no internal tools, label permissions or live CUBRID access. SECURITY.md lists email, GitHub private vulnerability reporting when offered, and a new detail-free "Security contact request" issue form as the fallback, which the issue chooser also links.
  • SQLAlchemy support claim matches the verified dialect range (#278) — SUPPORT_MATRIX.md, llms.txt, docs/index.md, README.md, docs/README.ko.md and the issue-template link now state SQLAlchemy 2.0 and 2.1 as supported instead of 2.0–2.2, matching the sqlalchemy-cubrid support contract. SUPPORT_MATRIX.md separates the supported range from installation ranges: sqlalchemy-cubrid main requires sqlalchemy>=2.0,<2.2 but that cap is unreleased, while the published 1.10.0 still declares <2.3, so its metadata would let pip install SQLAlchemy 2.2 once released, but 2.2 is not supported or verified by any recipe. scripts/check_support_matrix_counts.py (part of make check-docs) now fails when the SQLAlchemy row or the llms.txt/docs/index.md description promotes 2.2, and tests/test_support_matrix_counts.py covers a fixture that does. Historical entries are unchanged.
  • Korean README for the dashboard template (#123) — adds templates/dashboard/README.ko.md, a Korean translation of the dashboard README.
  • GitHub Release policy and standard CHANGELOG sections — AGENTS.md gains the shared "GitHub Release Policy" (Release titles equal their tag, stale-draft handling, release-note sections), and CONTRIBUTING.md documents the section list. This repository publishes no GitHub Releases and adds no release automation. The [Unreleased] headings were renamed (Docs to Documentation), merged by moving whole lines and put in the standard order; no entry was reworded.
  • Design: verify a driver release candidate before PyPI publish (#240) — docs/internal/rc-verification-design.md proposes an artifact (candidate) mode for the smoke-test.yml workflow-call contract: the driver's release workflow passes its built release-dist artifact and SHA-256 map, the cookbook verifies that exact wheel in the three release cells (CUBRID 11.2 and 11.4 on Python 3.12, CUBRID 11.4 on Python 3.11) with the unchanged smoke scope, and publish waits for the result; post-publish verification stays unchanged. It compares commit mode and TestPyPI staging, records the 2026-10-09 post-publish failures (PyPI index propagation, not recipe regressions), and lists the follow-up issues for this repository, pycubrid, sqlalchemy-cubrid and cubrid-mcp-server. No workflow behavior changes.
  • Dependency license inventory covers every requirements file (#217) — THIRD_PARTY_LICENSES.md was a single pip-licenses union of the template and some fundamentals requirements. It now covers all 43 requirements*.txt files, the CI/docs tooling the workflows and the PR-smoke action install directly, and the demo GIF renderer's imports: files with identical contents form 25 requirement sets, each installed into its own fresh environment (no flattened install), and the consolidated package table names the sets that pull each package in with every observed version. The new standard-library scripts/build_license_inventory.py regenerates it at a recorded commit, Python and OS, with resolution pinned by --exclude-newer, using the generator shared with pycubrid, sqlalchemy-cubrid and cubrid-mcp-server. MPL-2.0 (certifi, pathspec) is classified as weak file-level copyleft instead of under a blanket "no copyleft" statement; numpy (bundled libgfortran under GPL-3.0-or-later with the GCC Runtime Library Exception and libquadmath under LGPL-2.1-or-later), Pillow (bundled libraries, FreeType under the FreeType License) and protobuf are reviewed from their own license files. The document separates installed dependencies from material stored in the repository: docs/demo-agent-state.gif is drawn by this project's demos/render_gif.py from its own recorded output, with glyphs rasterised from a system font (DejaVu Sans Mono, else Liberation Mono, else Pillow's default). tests/test_third_party_licenses.py, run by make test-offline, fails when a requirements file or its declared packages, a workflow's or action's direct installs (folded, continued and quoted commands included), or the GIF renderer's imports are not reflected in the inventory, or when a category disagrees with the generator; version bumps do not fail it.
  • Flask template README matches the recipe layout (#135) — templates/flask/README.md no longer describes a nonexistent app/ package and /products HTML pages; it lists the eleven numbered recipes, shows how to run 01-basic-crud, and documents its test suite (including CUBRID_TEST_URL for live-CUBRID runs). Thanks @soyeladice-svg.
  • Docs site links repaired and internal pages unpublished (#154) — scripts/stage_docs.sh copied repository READMEs into docs/ unchanged, so their repository-relative links (fundamentals/connect/, SUPPORT_MATRIX.md, LICENSE, ../../GETTING_STARTED.md, the README demo GIF) no longer resolved after the move; mkdocs build --strict reported 51 of them at INFO level and passed because mkdocs.yml set not_found: ignore. Staging now lives in scripts/stage_docs.py (the shell script delegates to it): a link to another staged source points at its site page, a link to an authored docs/ file stays on the site, any other repository path becomes a GitHub URL on main, and a link to a missing path fails staging. mkdocs.yml now warns on unresolved, unrecognized and broken-anchor links, so --strict fails on them, and excludes docs/internal/ (the PRD) from the rendered site and search index. The new Docs site build (strict) CI job runs make docs on every PR and is part of the CI Gate; tests/test_stage_docs.py and the script's doctests (run by make check-docs) pin the rewriting rules, the missing-link failure, that sources are never modified, and the mkdocs.yml settings. CONTRIBUTING documents the site policy, including keeping the hand-maintained docs/quickstart.md site guide in step with GETTING_STARTED.md.
  • check_docs_sync.py now covers pitfalls/* example directories (#182) — pitfalls was excluded from CATEGORY_ROOTS because it used to be a single README, not a set of example subdirectories; #181 added a real recipe subdirectory, pitfalls/reserved-words/, that the gate never scanned. pitfalls is now a category root whose subdirectories are only treated as examples when they contain a .py file, so the README-only pitfalls/README.md landing page (and any future non-code subdirectory) stays exempt while pitfalls/reserved-words is scanned like every other example. README.md and llms.txt now reference pitfalls/reserved-words directly, and tests/test_docs_sync.py (run by make check-docs) pins both behaviors: an undocumented pitfalls example fails the gate, and a README-only pitfalls page passes.
  • AI guidance and MVCC isolation descriptions corrected (#150) — llms.txt and fundamentals/README.md described "all 6 numeric isolation levels plus a dirty-read demonstration"; they now describe the three levels CUBRID accepts under MVCC — READ COMMITTED (4, default), REPEATABLE READ (5), SERIALIZABLE (6) — and the recipe's no-dirty-read demonstration. The isolation recipe itself called SERIALIZABLE "full serializable" and said numeric codes are no longer accepted: the CUBRID 10.2/11.x manual documents SERIALIZABLE as identical to REPEATABLE READ snapshot isolation (write skew still possible), and codes 4–6 are accepted while 1–3 and READ UNCOMMITTED are rejected (all confirmed live on CUBRID 10.2, 11.2 and 11.4), so its docstring, printed description and golden are corrected. llms.txt also lists the previously missing templates/ai-agent, drops hardcoded recipe counts (the pycubrid "14" was stale) in favour of the SUPPORT_MATRIX Recipe Coverage table, scopes its verification claim to what CI actually checks instead of "every example", and states the reserved-word and async facts below. scripts/check_docs_sync.py (run by make check-docs) now fails when an example directory is missing from llms.txt, as it already did for README.md.
  • Executable reserved-word advice and async driver guidance (#151) — the pitfalls/ "correct" DDL still used the reserved word key unquoted and failed; it now uses setting_key. name is removed from the reserved-word table (it is not in the official 10.2/11.2/11.4 keyword lists and works unquoted), key/date/day are added, the three quoting forms CUBRID accepts (double quotes, square brackets, backticks) are documented, and the official 11.2 and 11.4 keyword pages are linked. The new golden-backed pitfalls/reserved-words/01_reserved_words.py runs every statement from that section under make verify (CUBRID 11.2 and 11.4 in CI; its SUPPORT_MATRIX row brings the table to 102 recipes, 66 golden-backed). The async pitfall no longer says CUBRID's Python drivers are all synchronous: it presents the native pycubrid.aio driver and the cubrid+aiopycubrid:// SQLAlchemy engine as the async options, keeps sync handlers and asyncio.to_thread for synchronous code, and retains the warning against calling the synchronous API inside async def.
  • SUPPORT_MATRIX recipe counts corrected and guarded (#126) — the Recipe Coverage table now covers every recipe (101 in total, 65 golden-backed; it previously claimed 68 and 45, and the prose said 62 and 46) and is the only place the counts are written; the prose and the docs home page point at it. The new scripts/check_support_matrix_counts.py, run by make check-docs (part of make check in PR CI), maps each table row to one counting rule (goldens per directory, the smoke workflow's */tests suite discovery for Flask and FastAPI, numbered scripts for the dashboard and AI agent templates) and fails when a row, the total, or an uncategorized golden disagrees with the repository. Thanks @alperensumeroglu.
  • Added a README "First contribution" guide (with Korean translation) pointing newcomers to the right sibling repo for their first PR, and documented the good first issue → status: in progress label lifecycle in AGENTS.md.
  • Clarified contributor evidence and maintainer responsibilities for project tools, labels, integration and translation follow-up; corrected the FastAPI recipe run path. Docs exemptions now require a real standalone reason rather than template/example text, with CI-executed helper regressions. Pinned the existing shared doc-lint/live-smoke callers to a reviewed upstream commit without changing their inputs or gates.
  • AI agent demo GIF embedded in README — agent state management showing sessions, JSON columns, SET tags.
  • 한국어 getting-started 사이트 페이지 (#109) — GETTING_STARTED의 3-경로 사이트 번역판을 docs/ko/getting-started.md로 추가하고 Project → Translations에 노출.
  • 한국어 README 추가 (docs/README.ko.md) — README 전문 번역으로 4개 Python 리포 모두 한국어 지원(접근성). 사이트 Project → Translations → 한국어로 노출. 번역 동기화 관리 장치 도입: 모든 번역 파일에 동기화 마커 + docs-sync에 translation-sync 잡 추가 — README.md가 바뀌는데 번역이 같은 PR에서 안 바뀌면 실패 (translations-deferred 라벨로 의도적 보류).
  • Docs site information architecture unified across the ecosystem — nav aligned to the shared six-tab skeleton (Home / Getting Started / Usage / Reference / Operations / Project): catalog/topic pages and the six templates under Usage, Support Matrix under Reference, Known Issues under Operations, Changelog under Project.
  • Added SUPPORT.md (help channels, templates, security pointer) — root-file parity with the sibling repos instead of relying on the org fallback.
  • Documentation site launched with full content indexing — mkdocs-material site at https://cubrid-lab.github.io/cubrid-cookbook-python/. scripts/stage_docs.sh stages the repo's real documentation (root README catalog, GETTING_STARTED, SUPPORT_MATRIX, KNOWN_ISSUES, CHANGELOG, fundamentals/performance/pitfalls topic READMEs, all six template READMEs — 19 pages) into the site on every build (CI docs.yml and make docs), so site search indexes the actual docs and the repo remains the single source of truth. README's private org-board link removed (404 for signed-out readers).
  • GETTING_STARTED.md restructured as the judge/demo entry point — three-path flow (pycubrid first query → ORM/templates → uvx cubrid-mcp-server with the four natural-language steps incl. the read-only refusal), expected-output pointers to the CI-checked goldens, and fixed stale fundamentals/connect/-style links that no longer matched the directory layout.

  • CUBRID server license line added; copyright notice unified (#99) — THIRD_PARTY_LICENSES.md states the verified upstream licensing (server engine Apache-2.0, APIs/connectors BSD per CUBRID's COPYING; GPL v2+ is outdated) and that the examples are independent clients with cubrid/cubrid images used for CI/demo only. LICENSE/NOTICE copyright unified to Yeongseon Choe, Gyeongjun Paik (2025-2026), replacing the CUBRID Labs line.

  • Added THIRD_PARTY_LICENSES.md and NOTICE — pip-licenses-generated inventory of the union of template/example requirements (all permissive; no GPL), and a NOTICE declaring the examples original with no embedded third-party source. Documentation only.

  • Corrected the async recipe's sqlalchemy-cubrid floor from >=1.0 to >=1.4.2 (#94) — fundamentals/async/02_async_sqlalchemy.py builds an AsyncEngine with the cubrid+aiopycubrid:// URL, but that async dialect (cubrid.aiopycubrid entry point) only became installable from PyPI in sqlalchemy-cubrid 1.2.3 — the entry points were missing from the 1.2.0–1.2.1 releases and 1.2.2 was yanked; 1.2.1 only shipped the get_pool_class()/create_async_engine() fix (#116). So the recipe's sqlalchemy-cubrid>=1.0 pin (in its requirements.txt, its printed "Requirements:" line, and the matching golden) advertised a version that cannot run it. Bumped all three to >=1.4.2, matching the floor already used by the other advanced SQLAlchemy recipes (pandas, ORM, Django, dashboard) rather than the bare 1.2.3 minimum. SUPPORT_MATRIX.md now carries an [^async] footnote documenting that the sync dialect works from 1.0 while the async recipe needs >=1.4.2. The global README badge stays at >=1.0: the sync cubrid+pycubrid:// recipes (including all FastAPI templates, which use create_engine, not create_async_engine) genuinely run on 1.0.

  • SUPPORT_MATRIX bug fixes (#57) — fixed the "Run all tests" commands, which chained two relative cds so the second (templates/api-service-fastapi/recipes) resolved against templates/flask/ and failed; each is now wrapped in a subshell. Added the previously-omitted async-worker (Celery) and batch-etl (Pandas) templates to the recipe-coverage table as manual-run entries and updated the total. (The version-badge/matrix and Python-table inconsistencies from the same issue were already resolved in #80 and #81.)
  • Reconciled the minimum sqlalchemy-cubrid version to >=1.0 (#80) — the README badge advertised ≥1.6.0 while SUPPORT_MATRIX.md listed ≥1.0, and scattered requirements.txt files pinned stale/invalid versions (>=2.0.0, which does not exist; >=0.4.1; >=0.3.0). Aligned the badge and the invalid/pre-1.0 stragglers to >=1.0 (the documented source of truth); feature-specific >=1.4.2 pins were left unchanged.
  • README fundamentals link renamed to "Parameterized queries" (#82) — the link targeting fundamentals/parameterized-queries/ was labelled "Prepared statements", implying server-side prepare that pycubrid does not do; relabelled to "Parameterized queries" with a client-side note to match the recipe's own README warning.
  • SUPPORT_MATRIX Python table fixed (#81) — added a 3.14 row and re-sorted the Python versions table into consistent descending order (3.14 → 3.9); previously 3.14 was missing and 3.13 was listed after 3.10.
  • SUPPORT_MATRIX corrected to match CI (#72) — the matrix claimed CUBRID 11.4 was "fully supported" and "all 62 recipes pass", but CI only runs make verify (46 stdout goldens) on CUBRID 11.2 / Python 3.12; the Flask/FastAPI/Streamlit/Django pytest suites and CUBRID 11.4 are never exercised in CI. Reworded the server/Python/recipe tables to state exactly what CI enforces vs. what is run manually or merely expected to work, removing the unenforced green checkmarks.

CI

  • Routine smoke runs CUBRID 11.4 only; weekly, manual and release runs keep 11.2 (#480) — a push to main and the daily schedule now run the full, fail-closed make verify in smoke-test.yml on CUBRID 11.4 / Python 3.12 only, and a push to main runs the pinned CQRS job (ci.yml, scripts/ci_scope.py) on 11.4 only. The daily 11.4 check stays (see "CI tiers" in CONTRIBUTING.md). The schedule is now two exclusive crons, daily 0 0 * * 0,2-6 and weekly 0 0 * * 1, the Monday of the weekly ci.yml schedule; the weekly run, manual runs and any other event or cron string keep CUBRID 11.2 + 11.4, and the weekly ci.yml run and manual runs keep CQRS on both. Release verification (repository_dispatch, a pinned manual run, workflow_call, candidate mode) is unchanged: the same three cells as scripts/release_smoke.py RELEASE_CELLS. Pull requests are unchanged. A new advisory Newest CUBRID image (advisory) job in the weekly (and manual full) smoke compares cubrid/cubrid:latest with 11.4 by digest and cubrid_rel engine version (scripts/latest_server.py). When both tags are the same image it names that digest as the newest-stable image and starts no database; every smoke cell now records the exact image digest and cubrid_rel it ran in its own summary, so the 11.4 cells count as that test only when they show the same digest. Only when latest has a newer engine build does it run a focused canary on that image's immutable digest. A failed lookup is reported as unverified, never as passed. It is continue-on-error, outside the release report, and never changes the support matrix or release cells. tests/test_ci_scope.py now evaluates the smoke matrix expressions for every event, including workflow calls made from a caller's push or schedule, and tests/test_latest_server.py covers the equal-digest, newer-build and unavailable-image cases; both fail on the previous workflow. Measured per-event job and minute changes are in CONTRIBUTING.md: a push to main drops from 4 to 2 live jobs (about 13 to 7 billed minutes), the schedules from 14 to 8 jobs a week (56 to 32 billed minutes, plus the new advisory job). SUPPORT_MATRIX.md, docs/index.md and the quickstart, performance and AI agent READMEs now state the per-event CUBRID versions.
  • Every new issue starts in status: needs triage (#481) — the bug and feature forms add the label, the new issue-triage.yml workflow adds it to every opened issue, and the driver-main lane adds it to the issues it files. Priority and size are finalized in maintainer triage, after which the maintainer removes the label.
  • The pull-request smoke jobs publish the live verification summary and keep test-results/ (#462) — Smoke Tests (CUBRID 11.4) and Smoke Tests (CUBRID 11.2) in ci.yml append test-results/verify/summary.md (the lanes the smoke action ran, from summary.json) to the job's step summary on success and failure, collect docker logs and the $CUBRID/log directory of the cubrid and cubrid-live containers into test-results/cubrid-logs/ after a failure, and upload test-results/ together with .live-evidence/ in the existing live-evidence-pr-cubrid-<version> artifact (same pinned actions/upload-artifact, if: always(), continue-on-error, 14-day retention). The artifact now holds test-results/ and .live-evidence/ folders instead of the bare .live-evidence/ contents. No permission, secret or trigger changes; smoke-test.yml, which sibling repositories call for release verification, is unchanged.
  • CI runs make verify and the focused live lanes (#416, #425, #426, #428) — smoke-test.yml (push to main, nightly, manual runs and release verification; CUBRID 11.2 and 11.4) now runs the full, fail-closed make verify instead of the golden-only check plus hand-written pytest steps: every golden, then the FastAPI quickstart, migration and performance, Flask/FastAPI, Django/Celery, AI agent and dashboard suites one lane at a time, on three job-owned CUBRID containers of the matrix version (the examples database on localhost:33000, CUBRID_TEST_URL on :33100 and CUBRID_DASHBOARD_TEST_URL on :33200). The dashboard's own container replaces the earlier stop-and-restart of the examples database. make deps installs every example's requirements under the selected driver constraints, and the workflow no longer exports VERIFY_PATHS, which would make make verify partial. make verify-ai-agent then runs a second time on the same database, and both runs keep their evidence. The ci.yml pull-request smoke lanes run make deps, make verify-goldens VERIFY_PATHS=<roots> and the focused lanes scripts/ci_scope.py selects in its new smoke_lanes output: make verify-quickstart for quickstart/5min-fastapi/ (other than Markdown), make verify-performance for performance/ and tests/test_performance_benchmarks.py, and make verify-ai-agent twice on a dedicated second container for templates/ai-agent/ and tests/test_ai_agent.py. Each of these paths also runs every golden, even when a golden root changes beside it, and wider reasons to run the smoke lane select all three lanes; fundamentals/parameterized-queries is the only example directory left without a live consumer. Every smoke job uploads .live-evidence/ once (actions/upload-artifact at the pinned v7.0.2 commit, if: always(), continue-on-error: true); a release workflow call uploads none, so no artifact is written into the caller's run and the release smoke result never depends on the upload. Permissions are unchanged (contents: read). The other pull-request recipe jobs keep their per-recipe environments.
  • Optional live grading and manual agent runs for the recipe evaluations (#352) — scripts/agent_eval.py run --live runs a task's live checks through the worktree's own make up / make verify / make clean, with a time limit per step, and records the requested CUBRID version, the image that really ran and the server's cubrid_rel line (a mismatch, such as an old base pinning 11.2 when 11.4 was requested, becomes a record warning, a summary count and a failed claim for the requested version), the current scripts/ci_scope.py selection (verify_paths, suites) and the exact commands. A make verify counts as passed only when it compared a golden with none failed or skipped. prompt gives an agent the task, issue, recipe and report format without the grading key. record stores one run: tool and model versions, task revision and prompt digest, start SHA, commands, skipped live checks, changed files, wall clock, time limit, review rework rounds, review findings, an optional exclusion for an unfair run, and the rubric result. summarize reports repeated runs per task revision and model as counts and a 95% Wilson interval, never a bare pass percentage. agent-evals/README.md describes the manual-only protocol: an isolated clone without later history or a push remote, no credentials, a fixed time limit, and publishing only when allowed. The isolation task gains a fixture from a real live run. Nothing runs an agent or the live path in CI.

  • Offline agent evaluation tasks and grader (#352) — agent-evals/ adds a versioned task format for benchmarking coding agents on frozen historical recipe fixes, and scripts/agent_eval.py grades a candidate's patch, test report and dry-run result. It checks the task's evaluator-owned hidden tests at its base SHA (a pytest run that skips every test is not a pass), the changed-file scope, required goldens and documentation, documented dependencies, values interpolated into SQL, hardcoded secrets, cookbook_ table names, and honest live evidence: an offline result reported as make verify or a live suite, a live claim without a recorded run (checked clause by clause), or a missing not verified statement fails. Three tasks come from merged fixes: the ai-agent reserved column and TEXT type (#125), the MVCC isolation levels and their golden (#54) and the async recipe's sqlalchemy-cubrid floor (#94). Their fixtures cover a missing golden, an unsafe query, an undocumented dependency, an unrelated-file change and false claims. make agent-eval-check re-grades them deterministically and runs in make test-offline; it calls no model and needs no database. scripts/ci_scope.py and every CI lane are unchanged, and no score gates a pull request. See agent-evals/README.md.

  • The documentation check's shell scanner matches pycubrid again (#405) — scripts/_markdown.py takes pycubrid's final _substitution_end() byte for byte (cubrid-lab/pycubrid#978): ;& and ;;& end a case clause, a word continued by a quote or backslash (esac"x", esac\*) is a pattern rather than esac, and a # comment at a word boundary inside a substitution changes nothing, with word boundaries tracked across escapes and quotes. The file now differs from pycubrid's and sqlalchemy-cubrid's only in its header line and one issue reference; pycubrid's 13 further regression cases are added. No other checker rule changes.
  • The documentation check reads shell substitutions as pycubrid and sqlalchemy-cubrid do (#403) — scripts/_markdown.py now takes the rest of the shared parser's shell handling (cubrid-lab/pycubrid#947, cubrid-lab/sqlalchemy-cubrid#935): a case pattern's ) does not close a $(...), substitutions inside double quotes run (single-quoted ones do not), text the tokenizer rejects names no make goal, arithmetic $((...)) runs only the substitutions nested in it, case and esac are keywords only at the start of a command, and an unquoted newline in a substitution body separates commands. Review of the port found two more gaps in the shared _substitution_end(), fixed here: case inside arithmetic ($((case + 1))) is a variable name, not a keyword, esac closes a case with no pattern ($(case x in esac)) but is a pattern after ( or | ((esac), x|esac)), and a quoted subject (case "x" in) is one word; these used to drop the make goals of the whole command or report words as goals. The function now tracks arithmetic parentheses and each case's subject, in, clause start, pattern and body, and the unused _keyword_at() is removed; the same change goes to the pycubrid and sqlalchemy-cubrid copies so they stay identical. 22 new make_goals cases cover these rules (7 failed on the old parser, 8 more on the pycubrid version). The checker's other rules are unchanged.
  • The documentation check computes site anchors with this site's slugifier (#388) — scripts/_markdown.py, the parser shared with pycubrid and sqlalchemy-cubrid, modelled every site anchor on pymdownx.slugs.slugify, which those repositories configure. This site's mkdocs.yml sets no slugify, so MkDocs uses Python-Markdown's default: ## Résumé is #resume, not #résumé, and a Korean heading has no letters left and is numbered #_1, #_2. A valid site link to such an anchor failed docs-check. The parser now reads the repository's mkdocs.yml and uses the slugifier it configures (default_site_slug() here; pymdownx.slugs elsewhere), and allocates site ids with the toc extension's own rule: an empty id becomes _1, and a taken _N is incremented (_1 then _2). GitHub anchors are unchanged. Tests cover an accented and a Korean heading under both slugifiers, the mkdocs.yml detection, and agreement with Python-Markdown's own slugify when it is installed.
  • Deterministic documentation defects block the merge (#353) — a new always-on docs-check job in ci.yml runs scripts/check_docs_links.py, which is shared with pycubrid and sqlalchemy-cubrid. It reports broken relative links and anchors (a docs/ link to a staged page resolves to its source), undefined references, unclosed fences, issue-number lines that the site would render as headings, missing make goals and scripts/ paths, and recipe commands whose pip install -r file, python script or uvicorn module does not exist. It runs offline and also on rename- and delete-only pull requests. Reviewed exceptions live in scripts/docs-check-allowlist.txt with a reason each; inline markers are not honoured. The shared doc-lint stays advisory for style. CONTRIBUTING.md describes which check owns what, and a stale script reference in its CHANGELOG section is corrected.
  • agent-review no longer counts empty reply-only Copilot records as review rounds (#380) — a Copilot record with no body and no thread-opening comment, such as a reply to an existing thread, is now ignored like an error or quota notice instead of counting as a summary-only round, which could put a PR past the two-round budget and reject valid evidence. A record that opens a thread still counts. Ported unchanged from cubrid-lab/cubrid-mcp-server#349, so the gate copies stay identical outside their constants.
  • Advisory agent-review check for review evidence (#351) — .github/workflows/agent-review.yml runs scripts/agent_review_gate.py from the base branch with a read-only token on every pull request and review event, and reports whether GitHub's own review records meet the review route of 2026-10-15: a substantive Copilot review within the two-round AI review budget, or a maintainer's APPROVED review, or a COMMENTED review that opens an inline thread. Changes to any workflow or composite action, .github/dependabot.yml, scripts/release_smoke.py, scripts/mcp_smoke.py, the lane selection in scripts/ci_scope.py and scripts/driver_floors.py, and the gate itself need a maintainer's APPROVED review. Pending requests, PR-body checkboxes, summary-only bot text, other people's reviews and stale reviews never count; unresolved or author-resolved critical or security threads and missing API data fail the check. Dependabot PRs are exempt. A review thread without author identity, a resolved one without resolver identity, or a commit without author or committer identity fails closed. The gate is a copy of the cubrid-mcp-server gate (Refs cubrid-lab/cubrid-mcp-server#341) with only its constants block changed (the thread-identity fix is being ported back in cubrid-lab/cubrid-mcp-server#346), and make test-offline runs its tests. The check is advisory: a pull request can publish a check with the same name, so it must not be made a required check until it is posted from an identity a PR cannot impersonate (#378). CONTRIBUTING.md documents the rules.
  • Pages permissions only on the deploy job, and labelled automated issues (#330) — .github/workflows/docs.yml granted pages: write and id-token: write to the whole workflow, so the build job that runs pip install and MkDocs had them too; the workflow is now contents: read and only the deploy job gets the two Pages permissions. tests/test_workflow_permissions.py allowlists them for deploy instead of the top level, so a top-level or build grant now fails. The driver-main.yml report job opened its tracking issue with only --label ci; it now also adds priority: medium and size: S, as AGENTS.md requires at creation (CONTRIBUTING.md says so), and a new test fails when any workflow's gh issue create lacks exactly one canonical priority: and one size: label. Both tests fail on the previous workflows.
  • scripts/check_translation_sync.py has offline tests — tests/test_translation_sync.py covers the BASE_REF handling of changed_files() (default main, origin/ prefix added once, tab-separated lines contributing both paths, a failing git raised) and main() (no translations present, unchanged README.md, a missing Korean translation failing, a missing community translation only warning, an unknown language labelled by its code). The tests run in make check-docs and in the docs-sync workflow. The script is unchanged.
  • The closing guard skips a multi-line link reference definition — CommonMark lets a definition's destination and title run on to later lines, so Closes, a [a]: line, an indented /target line and then owner/repo#N still renders nothing between the keyword and the reference. The gap walk now reaches every later line end of the paragraph after a definition line, up to a blank line, which can only add blocking; each line end is scanned once per check. Copilot found this in cubrid-lab/sqlalchemy-cubrid#822; pr-title.yml stays byte-identical across pycubrid, sqlalchemy-cubrid, cubrid-mcp-server and cubrid-cookbook-python.
  • The closing guard catches a closing keyword used as link text — [Closes](url) owner/repo#N renders as "Closes owner/repo#N", but the gap after the keyword stopped at ]. The PR title check now crosses a link's ], ](destination) or ][label], and also a quoted continuation line, an image's alt text and a link reference definition line, each of which renders nothing between the keyword and the reference. Scanning stays linear in the body length, so adversarial link syntax at GitHub's 65,536-character limit stays well under the tests' 5-second timeout. The fix came from Copilot's review of cubrid-lab/pycubrid#849, and pr-title.yml is again byte-identical across pycubrid, sqlalchemy-cubrid, cubrid-mcp-server and cubrid-cookbook-python.
  • The closing guard also checks the PR title and commit messages, and owners with an underscore — the PR title and the PR's commit messages make up the squash commit, so the PR title check now reads them too, as plain text: Markdown code does not hide a keyword there, and each commit message is checked on its own. The job lists the commits with gh api, using a token that can only read pull requests (the job has pull-requests: read; the workflow-level permissions stay empty), and fails when a PR has 250 or more commits, because GitHub lists only
  • A test pins that step. Owners with an underscore, as Enterprise Managed Users have (alice_acme/repo#N), are now matched in references and links. The job setup and these checks come from pycubrid's guard (cubrid-lab/pycubrid#845); code-block and comment handling of the PR body is unchanged; an underscore inside a word is no longer read as emphasis, so underscore owners are matched there too. cubrid-mcp-server (cubrid-lab/cubrid-mcp-server#276) and sqlalchemy-cubrid (cubrid-lab/sqlalchemy-cubrid#818) have the same change.
  • The PR-body closing guard catches emphasized, linked, escaped and split closures, and fails closed where Markdown structure is uncertain — the PR title check now rejects:
  • emphasis or HTML around the keyword or the reference (**Closes**, *Fixes*, __Resolves__, Closes **owner/repo#N**, <b>Closes</b>), a keyword split by markup (Close**s**), and inline comments, processing instructions or CDATA between the keyword and the reference;
  • www. and scheme-less github.com/ issue URLs, autolinks, backslash escapes (\#), HTML entities (&#35;, &nbsp;), a backslash line break, leading zeros, and a foreign issue listed after another one (Closes #1, owner/repo#2);
  • a closing keyword followed by a link ([text](…), [text][label], <a href>) when the body links any foreign issue, absolute or relative, since the link text hides its target. This case has its own error message.

Only fenced and indented code blocks and HTML comment blocks are ignored. Inline code and inline comments are scanned, because hiding them needs inline parsing that link destinations, HTML attributes and autolinks can defeat. Once a fence appears where its extent is unknown (in a list item, a block quote, after a list marker, inside raw HTML, on an indented line, or as a line that only looks like a fence), no later fence or comment is hidden, so a closing fence can never be misread as an opener. Raw HTML blocks end where CommonMark ends them, and any other line starting with a tag is treated as raw HTML until a blank line. Comments after anything that may start an HTML tag are scanned, a comment also ends at --!>, and after a raw-text element such as <textarea> or <select> the rest of the body is scanned. Lines split only at CR and LF, a leading byte order mark is dropped, and only spaces and tabs count as blank or indentation. A heading, block quote, fence, thematic break or comment block directly under a list item now ends the list, so indented code after it is ignored instead of blocking the PR. Footnote text is treated like list text. A very long issue number is reported instead of crashing the check, and long adversarial bodies stay fast at GitHub's 65,536-character limit. This is the same guard, with the same regression cases, as sqlalchemy-cubrid (cubrid-lab/sqlalchemy-cubrid#816) and cubrid-mcp-server (cubrid-lab/cubrid-mcp-server#275). A test pins the guard block's SHA-256 so every copy changes together. - scripts/lint_changelog.py no longer warns that there are no released versions — the warning was accurate but misleading: tags v0.1.0 and v0.2.0 were cut before CHANGELOG.md existed, and the repository publishes no GitHub Releases. Writing sections for them now would invent release notes that never existed (AGENTS.md "Release notes": historical notes are never regenerated, and a selective fix needs maintainer approval), so the lint lists them in PRE_CHANGELOG_TAGS and reports them instead. The changelog preamble names them too. The [Unreleased] entry for the eight Python examples now says they were already in the v0.1.0 tag. The script now differs from the pycubrid, sqlalchemy-cubrid and cubrid-mcp-server copies in this constant and message as well as in SECTION_POLICY_CUTOFF; only this repository has tags older than its changelog. - Reject cross-repository issue auto-closure in pull requests — the required PR title check prevents direct foreign closing keywords in PR bodies, while leaving local Closes #N and cross-repository Refs owner/repo#N unchanged. No PR code is checked out and no token permissions are granted. (Superseded in part by the entries above: the job now has a pull-requests: read token to list the commit messages, and the PR title and commit messages are checked too.) One tested scan skips fenced code and HTML comments (a marker inside one cannot hide live text in the other); indented lines stay checked. (Superseded in part by the guard entry above: an indented code block outside a list is now ignored.) Issue/PR templates clarify independent repository ownership. - ci.yml comments describe the main ruleset (#223 follow-up) — the comment above the two PR smoke lanes no longer says their check names are kept "so branch protection is unchanged": the main ruleset requires only CI Gate (all checks must pass) and PR title, and ci-gate enforces the smoke lanes. The ci-gate and dependabot-auto-merge.yml comments now name the ruleset. Comments only; no job, check name or trigger changed. - ci.yml installs dependencies with a pinned uv (#229) — the lint-python, quickstart-tests, docs-site and pytest-suites jobs run astral-sh/setup-uv v10.2.0 (pinned to its commit SHA, uv 0.12.17, after actions/setup-python, with the job's python-version and a per-job cache cache-suffix: ${{ github.job }}) and install with uv pip install --system; uv pip freeze --system logs the resolved versions. The pytest-suites job keeps one fresh venv per suite (#285), now created with uv venv and filled with uv pip install --python. pip and uv resolve the same package set for the quickstart, Flask, FastAPI, Django and docs requirements (checked locally, names normalized). Left on pip on purpose: smoke-test.yml and .github/actions/pr-smoke (release verification and candidate provenance read pip's direct_url.json; make deps honours PIP_CONSTRAINT), driver-floors, driver-main.yml, docs.yml, and the setup-command inputs of the reusable live-smoke.yml jobs (they run in the organization's workflow, which does not provide uv). tests/test_workflow_installs.py pins the setup. No runtime change. - Pull requests run the changed Flask/FastAPI recipes' own suites (#227) — scripts/ci_scope.py now emits web_suites, a JSON list of the changed Flask/FastAPI suite roots (a directory under templates/flask/ or templates/api-service-fastapi/recipes/, or the root FastAPI app, that owns a requirements.txt and tests/). ci.yml's Recipe pytest suites (live CUBRID) job is now a plain job with its own CUBRID 11.4 service: for each selected root it creates a fresh venv, installs only that root's requirements.txt (plus pytest, pytest-asyncio, httpx), and runs its tests/ in a separate pytest process; every suite runs even after a failure, the job fails if any suite fails or passes no test, and CI Gate (all checks must pass) fails with it. Shared Flask/FastAPI files (for example templates/flask/requirements.txt), shared live infrastructure, CI-policy changes and unknown paths add the representative suites (templates/flask/01-basic-crud, templates/api-service-fastapi/recipes/01-basic-crud and the root FastAPI app) on top of the changed recipes; they never count toward the cap. At most MAX_WEB_SUITES (25) changed roots run per pull request, which covers every current suite (23; a test fails when a new recipe outgrows the cap), so a grouped Dependabot bump runs every suite it touches. Changed roots beyond the cap would be emitted as web_suites_skipped, and the job prints a ::warning:: for each plus a "Not validated before merge" list in its summary, pointing to a manual smoke-test.yml run on the branch; smoke-test.yml still runs every suite after merge. Each suite's pytest run is limited to 10 minutes, and the zero-passed guard reads only pytest's final summary line. Recipe 10 stays on its own pinned CQRS job. The roots reach the job only through an env var holding JSON, never through shell interpolation. Previously every Flask/FastAPI pull request ran only the representative suites with one shared install. tests/test_ci_scope.py covers single and multiple recipes, the shared-file fallback, recipe 10, non-recipe changes, a shared file plus three recipes, the grouped Flask bump, the cap and its skipped list, deleted and renamed recipes, spaces in file names, git-quoted paths, unsafe roots and the job wiring; CONTRIBUTING.md "CI tiers" and SUPPORT_MATRIX.md describe the lane. - Candidate artifact mode for the release workflow call (#240) — smoke-test.yml gains four optional workflow_call inputs: mode (published, the default, or candidate), candidate_artifact, candidate_hashes and candidate_source_sha (the commit the candidate was built from). Candidate mode also requires a request_id that stays the same across run attempts, so gh run rerun --failed replaces only the failed cells' parts. In candidate mode each of the three unchanged release cells downloads the caller's artifact by name from the same run, requires exactly the files in the build job's {"<file>": "<sha256>"} map with every SHA-256 matching, checks the single wheel's filename, archive integrity and METADATA name/version against package/version, and installs only that wheel (no PyPI wait, no index copy). After every later install the requested package must still be that wheel (direct_url.json and digest), every other driver must come from the package index, and the pip cache is restored but never saved. Missing, substituted, corrupt or mismatched input and incompatible mode inputs fail closed; published mode still installs only the exact PyPI release, and neither mode falls back to the other. New outputs evidence (published package / candidate artifact) and candidate_sha256; candidate artifacts are release-candidate-part-<request_id>--cubrid-<v>[-py<p>] and release-candidate-verification-<request_id>. The report keeps schema 1 with additive keys (evidence; per-part packages listing every installed distribution; and for candidates a candidate block with source_sha, expected_sha256 and the verified candidate_sha256, plus the caller repository and run commit); pushes and the schedule are labelled none (not release evidence); driver-main.yml labels its summary representative main (advisory). Existing callers are unaffected: same required inputs, outputs, job names, published artifact names, matrix and contents: read. tests/test_release_candidate.py covers wrong package, version, SHA-256 or digest map, missing artifact or report, dependency substitution, failed, cancelled or skipped cells and incompatible mode inputs. The contract and runbook are in docs/internal/rc-verification-design.md section 13, and the CONTRIBUTING "Release verification contract" points to it (once per mode per caller run). No caller uses candidate mode yet. - Release verification fails when the smoke result download fails (#232) — actions/download-artifact v8 can fail its digest check after extracting files, so valid-looking part files could still yield a passing report. The report job's download step (still continue-on-error, so the report always runs) now has id: download, and smoke-test.yml passes steps.download.outcome to scripts/release_smoke.py report --download-result. A release report fails unless that outcome is success, recording smoke result download did not succeed: <outcome>; the failure artifact is still written and uploaded. Non-release runs, RELEASE_CELLS, version matching and verify_result handling are unchanged. tests/test_release_verification.py covers failed, cancelled and skipped outcomes, the CLI argument and the workflow wiring. Callers pinning this workflow by SHA need a coordinated bump to pick it up. - Release verification waits until PyPI serves the requested version (design C4) — before installing package==version for a release request (repository_dispatch, a pinned workflow_dispatch or a workflow_call), scripts/release_smoke.py select now polls the PyPI JSON API (https://pypi.org/pypi/<package>/<version>/json, anonymous) until it lists exactly that version with a wheel or sdist, with 5/10/20/30 s backoff inside a 600 s wall-clock budget. HTTP and network errors, malformed JSON, another version or a release without files count as not yet served; on timeout the job fails with PyPI does not serve <package>==<version> after N s (<last reason>), where the reason names the HTTP/JSON error or what PyPI served instead (served <name> <version> with N wheel/sdist files, to diagnose stale CDN nodes), and never falls back to another version. On 2026-10-09 stale PyPI CDN nodes hid both driver 1.10.0 uploads from one smoke cell for about 95–106 s, exhausting the six install attempts and failing the release runs until the jobs were re-run by hand. The existing six-attempt install retry stays (the JSON API and the simple index can briefly disagree) and now uses pip install --no-cache-dir; the Select released drivers step timeout rises from 10 to 20 minutes to cover both budgets. Inputs, outputs, job names, RELEASE_CELLS, the report and the exact installed-version check are unchanged, and runs without a requested release never poll. tests/test_release_pypi_wait.py covers immediate and delayed availability, the timeout and its message, retried HTTP/JSON errors, rejection of other versions, packages or file-less releases, and that non-release and invalid requests never poll. - CHANGELOG lint applies the duplicate-heading check only after the cutoff — scripts/lint_changelog.py (rule 5) now rejects a duplicate ### heading within one version section only in [Unreleased] and in releases newer than SECTION_POLICY_CUTOFF (0.2.0), so released history is never rewritten; a duplicate heading before the first release header is still rejected. The script is now identical to pycubrid's, sqlalchemy-cubrid's and cubrid-mcp-server's except for SECTION_POLICY_CUTOFF and, since the entry above, PRE_CHANGELOG_TAGS and its message. tests/test_lint_changelog.py covers adjacent and non-adjacent duplicates after the cutoff, duplicates in released history, and the pre-release preamble; CONTRIBUTING.md documents the cutoff. - CHANGELOG lint rejects a release header inside an open code fence — scripts/lint_changelog.py now fails with Release header inside an open code fence in CHANGELOG.md when a line starting ## [ appears while a fence is open, because scripts/extract_release_notes.py is not fence-aware and would truncate the GitHub Release body at it. A fenced ### line stays entry content. The script now matches pycubrid's and cubrid-mcp-server's apart from SECTION_POLICY_CUTOFF (and, since the PRE_CHANGELOG_TAGS entry above, that constant and its message); tests/test_lint_changelog.py and CONTRIBUTING.md cover and describe the rule. - make check-docs lints the CHANGELOG sections — scripts/lint_changelog.py rejects non-standard, empty, duplicate or out-of-order ### headings and an unclosed code fence in [Unreleased] and in releases after 0.2.0. Also checks that [Unreleased] comes first and only once and that release sections are unique and in descending order. Covered by tests/test_lint_changelog.py. - Exact CQRS recipe dependency validation — the required CI gate now runs FastAPI recipe 10-cqrs-event-sourcing in isolated Python 3.12 jobs on live CUBRID 11.2 and 11.4, installing that recipe's own requirements.txt instead of the broad smoke dependency list. Each job checks package consistency, records installed versions, and runs its seven existing tests; SQLite fallback is not used. Broad latest-package smoke coverage remains separate. - Aggregate CI Gate (all checks must pass) job in ci.yml: a final if: always() job that needs lint-python, quickstart-tests, docs-sync, doc-lint and pytest-suites and fails unless every one of them succeeded (failure, cancelled or an unexpected skip all fail it; none of these jobs is path-gated). It mirrors pycubrid's gate so branch protection can require a single CI context alongside the two smoke-test jobs. The other jobs are unchanged. - Async SQLAlchemy recipe installs its required runtime (related to #152) — changed the existing SQLAlchemy requirement to sqlalchemy[asyncio]>=2.0, preserving its version floor while requesting the supported asyncio extra. Plain SQLAlchemy 2.1 omits greenlet and caused 02_async_sqlalchemy.py to fail on import in both live smoke cells. Updated the displayed requirements and matching golden, and documented the install target; all behavioral golden assertions remain intact. This fixes the caller install contract; it does not resolve the broader recipe-floor audit in #152. - Flask and FastAPI pytest suites run in smoke-test CI (#131): smoke-test.yml now installs the recipe test dependencies and runs every templates/flask/*/tests and templates/api-service-fastapi/recipes/*/tests suite (23 suites, 171 tests) against the job's live CUBRID container on both 11.2 and 11.4, using the CUBRID_TEST_URL support from #132 and #134. The suites run on pushes to main, the nightly schedule, upstream-release dispatches and manual runs; pull requests skip them so the PR gate stays fast. Each suite runs in its own pytest process; all suites run even after a failure and the step then fails, a suite that collects no tests also fails it, results are written to the job summary, and the step has a 15-minute timeout. The dependency list is explicit rather than each recipe's requirements.txt, whose pins conflict, and it leaves the released pycubrid and sqlalchemy-cubrid under test untouched. SUPPORT_MATRIX.md and the docs home page no longer describe these suites as manual-only. - cubrid-mcp-server now installs from PyPI in smoke tests (was git fallback). Falls back to the v0.4.0 git tag only if PyPI is unreachable. - Pinned ruff to 0.16.4 in CI (#78) — the lint job installed ruff unpinned, so formatter/linter rule changes in new ruff releases could break CI unpredictably; pinned to 0.16.4 to match the version used by pycubrid and sqlalchemy-cubrid. - Golden-coverage guard closes the silent-skip gap (#89) — added scripts/check_expected_coverage.py, wired into make verify (via a new check-coverage target) and the smoke-test.yml workflow. make verify is golden-driven, so any *.py inside a directory that owns an expected/ folder but lacks a matching expected/<name>.expected golden was silently never executed in CI. The guard now fails loudly on any such uncovered script (exceptions go in scripts/verify_exclusions.txt with a reason). - Smoke tests now run a CUBRID 11.2 + 11.4 job matrix — smoke-test.yml previously started a single cubrid/cubrid:11.2 container; it now runs make verify goldens against both 11.2 and 11.4, giving the "expected to work" claim for 11.4 in SUPPORT_MATRIX.md direct CI evidence. SUPPORT_MATRIX.md updated accordingly.

Tests

  • Nested Flask and FastAPI recipes receive individual smoke-coverage warnings (#475) — the docs-sync smoke check now finds runnable sub-recipes under templates/flask/ and templates/api-service-fastapi/recipes/ and warns when a recipe has no local tests/ or expected/, even if its siblings are covered. README/llms.txt indexing and the existing warning-only policy are unchanged.
  • Recipe tests are colocated with examples (#472) — AI-agent offline and live checks now live under templates/ai-agent/tests/, benchmark and migration regression checks under performance/tests/, and the output normalizer's shell test under tests/. The make test, make test-normalize, make verify-ai-agent, make verify-performance and make verify commands, their CI test selection and the existing golden files keep their behavior; this change only relocates tests and updates path references.
  • AI-agent live suite asserts persisted state, MCP semantics and repeatability (#422) — tests/test_ai_agent.py no longer passes on exit code and a ✓ ... working line alone. After each of templates/ai-agent/01, 03, 04 and 05 runs, an independent pycubrid connection reads the rows the recipe persisted (IDs and timestamps left out, foreign keys resolved through joins) and compares them with explicit expectations: the agent session, tags, messages with JSON metadata and tool calls; the RAG documents' source, metadata, tags and chunk count, each chunk's token count and embedding_ref, and the latest retrieval-log row; the agent loop's nine messages, three tool calls and product rows; the chatbot user, conversation and four turns; and every table's row count. The rerun case requires the same state after a second run on the same database, new root-row IDs (fixed keys deleted, not reused) and one more append-only retrieval-log row. A new MCP case runs initialize, tools/list (the scripts/mcp_smoke.py tool contract), all_table_names (its sorted list must equal the db_class user tables as owner.table, so same-named tables of different owners are not merged) and a SELECT COUNT(*) in process, and checks that the rejected DROP TABLE leaves the target table and a sentinel row unchanged; the MCP case now owns the agent-state tables for cleanup. CUBRID_LIVE_GATE=1 makes a missing CUBRID_TEST_URL fail instead of skip; without it the live suite still skips. make verify-ai-agent (and the full make verify) runs the live suite with the gate, writes JUnit evidence to .live-evidence/ai-agent/ (not tracked) and fails if any case skipped; the offline suite runs in make test. tests/test_ai_agent_offline.py checks that a deliberately wrong expectation fails each state comparison and that the gate fails without a URL or with an unreachable database. No recipe changed and no external LLM is called.
  • Live evidence for the migration goldens and the performance benchmarks (#423) — make verify-performance (and the full make verify) runs the new tests/test_performance_benchmarks.py against the examples' localhost:33000/testdb and writes a JUnit report and a summary to .live-evidence/performance/ (git-ignored). TestMigrationGoldens runs the five migration/java-to-python goldens as separately named tests with the make verify comparison; TestPerformanceBenchmarks has a named test for each of performance/fetch-optimization, bulk-insert and connection-pooling that runs benchmark.py under scripts/run_example.py and fails on a nonzero exit, empty output, a printed error or traceback, a missing comparison line, a time that is not a finite nonnegative number, or a workload that differs from the configured one. It never compares timings. Two more tests inject a failure into fetch-optimization and bulk-insert after their table exists and check that cookbook_fetch_test / cookbook_bulk_test are still dropped, and an unrelated sentinel table must be unchanged after each script-running test (a failure names that test). The report records the CUBRID server, Python, pytest and driver versions. The lane sets CUBRID_LIVE_GATE=1, so an unreachable database fails every database test (the run fails), and the lane fails if any test was skipped; without the variable the module skips when no database answers. The scripts use fixed table names, so the lane must not run at the same time as another make verify-performance, make verify or a manual benchmark run on the same database. No CI lane runs it yet. make help now also lists targets whose names contain digits. CONTRIBUTING.md and performance/README.md document it.
  • Rerunnable, fail-closed live lanes for the Flask/FastAPI, Django/Celery and dashboard templates (#421) — make verify-web, make verify-django-celery, make verify-dashboard and make verify-templates run scripts/live_gate.py, which reuses the existing suites (no new test implementations) from a declarative manifest, one pytest process per suite: verify-web runs every templates/flask/*/tests and templates/api-service-fastapi/recipes/*/tests suite on live CUBRID plus the root FastAPI template's startup test, which stays SQLite by design and is labelled startup-only (not live and not quickstart /health coverage); verify-django-celery runs templates/django/tests and templates/async-worker/tests, recorded as direct Celery task execution (.run() / eager .apply()) without Redis or a worker; verify-dashboard runs templates/dashboard/tests on CUBRID_DASHBOARD_TEST_URL, a separate database, because the suite drops and recreates its FK-linked tables. The Make targets set CUBRID_LIVE_GATE=1 by default (1/true/yes/on or empty/0/false/no/off; any other value is a usage error). Under the gate a missing, unreachable or non-cubrid+pycubrid:// URL (including SQLite), a dashboard URL naming the same database as CUBRID_TEST_URL, a database that already holds cookbook_* tables, or a suite that fails, collects no test, skips any test, leaves new tables behind or drops a pre-existing one (each suite against its own pre-run snapshot) fails the lane with a nonzero exit. The suites that print a database header (Flask, FastAPI recipes, dashboard) must print database: live CUBRID (...); the Django and async-worker suites print none and rely on skipping without a URL (a gate failure) and failing on a non-cubrid+pycubrid URL. The gate drops PYTEST_ADDOPTS and PYTEST_PLUGINS from the suites' environment. Only a gated run can PASS: with CUBRID_LIVE_GATE=0 the suites keep their existing skips and SQLite fallbacks and the summary says INCOMPLETE at best. Each run replaces only .live-evidence/<lane>/ (ignored by version control; other lanes' evidence is kept) with per-suite JUnit XML and logs plus summary.json/summary.txt: commands, commit SHA (dirty flag ignores untracked files), the Python and driver/framework versions of the interpreter that runs pytest, the CUBRID server version (SELECT VERSION()), and selected/passed/failed/errors/skipped counts and test-case outcomes per suite. The Django and async-worker fixtures now assert that their teardown removed their tables, so a cleanup gap fails the run that caused it instead of the next run's preexisting-table refusal. tests/test_live_gate.py (in make test) covers the fail-closed rules with a stub pytest and no database. CONTRIBUTING.md documents the lanes. No workflow changed.
  • The workflow issue-label check rejects non-canonical variants (#344) — label_problems() in tests/test_workflow_permissions.py required one canonical priority: and one size: label on every workflow gh issue create but ignored other labels, so priority: medium next to a forbidden variant passed. It now also reports any label that reads as a priority or size label without being a canonical name (priority:high, priority-high, Priority: High, priority: urgent, P1, p0, size:S, size-xs, Size: S, size: xs); unrelated labels such as ci, area: templates or pricing are not affected. A new test covers each variant and fails on the previous helper. No workflow changed: the current tracking-issue labels are canonical.
  • Offline test for the SQLAlchemy quickstart's cleanup after a failed drop (#332) — tests/test_quickstart_cleanup.py (in make test-offline) runs quickstart/5min-sqlalchemy/app.py's main() against a throwaway SQLite file and makes the Core, then the ORM, cleanup drop raise SQLAlchemyError. Each time the other drop still runs, engine.dispose() is called once, Cleanup failed: is printed and only the failed table remains. It needs only sqlalchemy, and it fails if the two drops share one try. The app is unchanged.