All notable changes to SysNDD are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning (loosely, in the 0.x line — additive changes land as patch bumps while the public API still stabilises).
Nothing yet.
0.11.14 — 2026-04-24
Patch bump for the durable async job hard cut in PR #305. This replaces process-local async job ownership with a MySQL-backed durable queue/state model, a dedicated worker service, and CI smoke/bootstrap fixes required to verify the new architecture on pristine environments.
- Pristine DB smoke startup no longer fails before foundational schema exists. Added a bootstrap migration so a fresh MySQL instance creates the base tables needed by later migrations before content and durable async schema migrations run.
- Production-style smoke verification now matches real stack readiness. The smoke script retries SPA readiness before asserting security headers, and local prod-stack compose starts rebuild fresh images so stale dev-built images do not produce false failures.
- Durable async job status reporting preserves terminal semantics. Cancelled jobs now report as
cancelledinstead of generic execution failures, and worker progress/lease updates are persisted so long-running jobs do not look stale while still executing.
- Async jobs are now durable and worker-owned. Canonical async state lives in MySQL, the worker service claims and executes jobs independently of the API process, and frontend polling no longer depends on sticky-session correctness.
- Worker deployment model is explicit. Operator docs now describe the separate worker service, durable queue semantics, and worker-health expectations rather than the old API-process
miraiownership model.
0.11.13 — 2026-04-23
Patch bump for the auth query-string hard cut in PR #304, plus a follow-up CI fast-path refactor so normal review cycles no longer wait on the full API suite and environment bootstrap checks.
- Auth-sensitive inputs are hard-cut to body-only transport.
POST /api/auth/signup,POST /api/auth/authenticate, and password-update flows no longer accept query-string payloads. Runtime request logging keeps auth-sensitive query strings redacted, and the DB cleanup migration scrubs persisted historical values. - Auth endpoint validation now fails closed on malformed JSON bodies. Signup and password-update handlers reject nested or non-scalar required fields with
400before downstream coercion or tibble/pivot work. - Auth review regressions are locked in. E2E auth lifecycle tests now target the mounted
/api/auth/*routes, and endpoint-auth tests assert unique source decorators before slicing source windows.
- PR CI now has a fast API gate. Pull requests run
Test R API (fast PR gate)against a repo-owned file selection, while non-PR CI keeps the full API suite. This preserves full confidence coverage off the PR critical path. - Tooling-heavy PR checks are conditional.
make doctorandSmoke Test (prod stack)only stay in the PR path when workflow, tooling, or boot-path files change; they still run outside PRs. - Local fast-loop mirrors PR CI.
make test-api-fastnow exists as the local equivalent of the PR-fast API gate, andmake pre-commituses it.make ci-localremains the full local parity gate.
Patch bump for the consolidated dependency + security sweep in #298 (subsumes #288, #289, #291, #292, #293, #295, #297). Also lands two CI stability guardrails introduced during review.
- Master CI break —
@testing-library/dompeer-dep drift. Vitest suites were failing withCannot find package '@testing-library/dom'becausenpm ci --legacy-peer-deps --prefer-offlinestopped hoisting it under@testing-library/user-event. Pinned as an explicitdevDependencyso the package is guaranteed at the root ofnode_modules. Seventeen previously failing test files now pass. - nginx: security headers silently dropped. Headers declared at
http{}level were being discarded by everylocation{}block because those blocks define their ownadd_header Cache-Control(nginx inheritance rule: a childadd_headererases the parent's). Moved the full OWASP / UniBE header set intoapp/docker/nginx/security-headers.confandincludeit inside every location — closes #296. - nginx: server version leak. Added
server_tokens offso theServer:header no longer advertises the exact nginx build. - API crashloop —
mirai 2.5.3/nanonext 1.8.2mismatch. PPM prunednanonext 1.7.2,mirai'sImports: nanonext (>= 1.7.2)floor was satisfied by the 1.8.2 release which no longer exports.read_header, andrenv::restoresilently upgraded on rebuild. Date-pinned the PPM snapshot URL (/cran/2026-04-22) in bothapi/Dockerfileandapi/renv.lock, and bumped tomirai 2.6.1+nanonext 1.8.2. Closes #294.
- Dependency bumps (prod):
@unhead/vue 3.0.3→3.0.4,bootstrap-vue-next 0.44.2→0.44.6,dompurify 3.3.2→3.4.0(mXSS + prototype-pollution fixes),swagger-ui/swagger-ui-dist 5.32.0→5.32.4. - Dependency bumps (dev):
axios 1.13.6→1.15.1(header-injection + CRLF hardening),eslint 10.0.2→10.2.1,msw 2.12.10→2.13.4,postcss 8.5.8→8.5.10,prettier 3.8.2→3.8.3,typescript 6.0.2→6.0.3,typescript-eslint 8.58.2→8.59.0,vue-tsc 3.2.5→3.2.7. - Docker image bumps:
mysql 8.4.8→8.4.9(prod + both dev DBs),axllent/mailpit v1.29.6→v1.29.7,fholzer/nginx-brotli v1.29.8→v1.30.0. - HSTS policy. Rewrote the header set.
Strict-Transport-Securitynow shipsmax-age=63072000; includeSubDomains; preload(2 years). Preload submission is deliberately deferred — see #300 before adding the domain to the browser preload list. Dropped legacyX-XSS-Protection,X-Download-Options,X-Permitted-Cross-Domain-Policies, and the duplicateX-Content-Security-Policyheader (all obsolete per modern browser guidance).
- CI smoke-test asserts SPA security headers.
scripts/ci-smoke.shstep 4 curls the running prod stack and grep-asserts presence ofStrict-Transport-Security,X-Content-Type-Options,X-Frame-Options,Referrer-Policy,Permissions-Policy,Content-Security-Policy, plus absence of aServer: nginx/<version>leak. Guards against a futurelocation{}block forgetting theincludeor aserver_tokensregression. - Dependabot: group container-image bumps. Weekly runs now open at most one Dockerfile PR and one compose PR (matching the existing
actionsgroup pattern) instead of one PR per image. - Dependabot: ignore Vite semver-major. Vite 8 has real breaking changes (
manualChunkstype moved from object to function,vitest.config.tsfactory API changed). Dropped here; reopen when we schedule a migration. Closes #290 (the standing dependabot PR).
- #299 — CSP tightening (drop
'unsafe-inline'/'unsafe-eval'); needs Report-Only probe + swagger-ui audit. - #300 — HSTS preload-submission decision (one-way door, needs UniBE ICT sign-off).
Closes Phase D of the v11.0 test foundation initiative by landing D6 (extract-bootstrap), plus a reinforcing Phase B worktree that closes six MSW handler-table gaps Phase C batch review identified. No new runtime behavior; D6 is a pure structural refactor of the startup path and reinforcing-B adds test-only MSW stubs.
- D6 — Extract
api/start_sysndd_api.Rintoapi/bootstrap/module set. Rewrote the 992-LoC startup script with 21<<-super-assignments into a 137-LoC thin composer over 8 bootstrap modules. Every<<-is eliminated —bootstrap_*()functions return their results and the composer binds them at the top level ofstart_sysndd_api.R(which IS.GlobalEnv), so endpoint handlers, filters, and middleware that still look uppool/serializers/migration_status/root/ etc. as globals keep working unchanged.api/bootstrap/init_libraries.R(76 LoC) —library()attachment order (STRINGdb/biomaRt first sodplyr's masks win).api/bootstrap/load_modules.R(144 LoC) — sources repositories → services → core → filters in the order the Phase C source-order test expects.api/bootstrap/create_pool.R(50 LoC) — builds the DBI pool, returns it.api/bootstrap/run_migrations.R(159 LoC) — runs pending migrations, returns status list.api/bootstrap/init_globals.R(63 LoC) — serializers, inheritance/output/user allow-lists,version_json/sysndd_api_version.api/bootstrap/init_cache.R(103 LoC) — disk-backedmemoisecache + 9 memoised helpers.api/bootstrap/setup_workers.R(132 LoC) — mirai daemon pool +everywhere({...})worker-side source block (unchanged function set and order, verified against the pre-D6 block).api/bootstrap/mount_endpoints.R(191 LoC) — allpr_mount()calls + filter wiring, returns the root router.api/core/filters.R(294 LoC, new) — extracted Plumber filter definitions (cors, auth, logging, error handler) fromstart_sysndd_api.R.api/Dockerfile— addedCOPY services/(pre-existing gap — container was relying on bind-mount alone, which would break production builds) andCOPY bootstrap/lines so the built image includes the new module directory.docker-compose.yml— added./api/bootstrap:/app/bootstrapbind-mount and a matchingdocker compose watchsync target.
- Reinforcing Phase B — 13 new MSW handlers covering six B1 gaps Phase C specs worked around via per-test
installAuxHandlersstubs. These are test-infrastructure-only changes; Phase E rewriting agents can now rely on shared mocks instead of duplicating per-spec stubs.- Gap 1:
GET /api/entity?filter=...(Review.vue step 1). - Gap 2-4:
GET /api/list/entity,/list/gene,/list/disease(dropdown stubs — these routes are Phase E contracts thatlist_endpoints.Rdoesn't implement yet; handler shapes follow the{id, label}tree-mode convention). - Gap 5:
GET /api/re_review/table(cursor envelope mirroringre_review_endpoints.R @get table). - Gap 6:
ManageAnnotations.vueaux endpoints —GET /api/admin/annotation_dates,/admin/deprecated_entities(with Viewer 403 branch);PUT /api/admin/update_ontology_async(Viewer 403 branch),/admin/force_apply_ontology(400 whenblocked_job_idmissing);POST /api/admin/publications/refresh(400 when body missing);GET /api/publication/stats,/publication,/publication/pubtator/genes,/publication/pubtator/table,/comparisons/metadata. - New fixture files under
app/src/test-utils/mocks/data/:lists.ts,re_review.ts,annotations.ts. - No existing B1 handlers modified; 33 test files still pass (439 passed + 6 todo).
- Gap 1:
wc -l api/start_sysndd_api.R→ 137 (plan target ≤200).grep -c "<<-" api/start_sysndd_api.R→ 0.grep -rn "<<-" api/start_sysndd_api.R api/bootstrap/→ 0 hits.Rscript --no-init-file api/scripts/lint-check.R→ 90 files, 0 issues.- Full backend test suite inside the api container: 70 files, 0 failures, 2338 passed, 247 skipped (the skips are the documented DB-gated / slow tests).
docker compose restart api→ boots in 3 attempts, zero "could not find function" or fatal-error entries in startup logs.- Critical endpoints all return 200:
/api/health/ready,/api/version/,/api/llm/prompts(D1-regression guard),/api/backup/list?page=1(D5-shape guard),/api/search/CUL1?helper=true. - CI on PR #256 (D6):
Detect Changes,make doctor,Smoke Test (prod stack),Lint R API,Test R APIall SUCCESS; frontend jobs correctly skipped (no frontend changes).
- Local and remote
v11.0/phase-d/*branches: 0. - Legacy wrapper file (
api/functions/legacy-wrappers.R): deleted. - All D1/D2/D3 split-file size targets met; the two documented overruns (
response-helpers.R762 LoC,llm-service.Rorchestrator 724 LoC) remain because splitting further would fragment cohesive logic.
Phase D of the v11.0 test foundation initiative — backend structural refactors protected by the Phase C test net. Five parallel worktree units (D1–D5) consolidated here; D6 (extract-bootstrap) follows in a subsequent PR. No new runtime behavior; this is exclusively source-structure refactoring protected by pre-existing Phase C tests.
- D1 — Split
api/functions/llm-service.R(1,748 LoC) into focused modules.llm-client.R(318 LoC) — Gemini HTTP/SDK calls:get_default_gemini_model(),generate_cluster_summary(),is_gemini_configured(),list_gemini_models().llm-types.R(572 LoC) — ellmertype_objectspecs + prompt builders.llm-rate-limiter.R(184 LoC) —GEMINI_RATE_LIMITconfig +calculate_derived_confidence().llm-service.R(726 LoC orchestrator) —get_or_generate_summary(), cluster data fetchers, prompt template CRUD.- Conditional source guards let the orchestrator load its dependencies when sourced standalone (e.g., from
test-llm-batch.R).get_api_dir()fallback handles testthat's working-directory switches.
- D2 — Split
api/functions/helper-functions.R(1,440 LoC) into 4 focused modules.account-helpers.R(194 LoC) —random_password,is_valid_email,generate_initials,send_noreply_email.entity-helpers.R(222 LoC) —nest_gene_tibble,nest_pubtator_gene_tibble,extract_vario_filter,get_entity_ids_by_vario.response-helpers.R(766 LoC) —generate_sort_expressions,generate_filter_expressions,select_tibble_fields,generate_cursor_pag_inf,generate_tibble_fspec. Exceeds the 500 LoC target becausegenerate_filter_expressions()alone is ~340 LoC; splitting it would fragment cohesive dispatch-table logic.data-helpers.R(291 LoC) —generate_panel_hash,generate_json_hash,generate_function_hash,generate_xlsx_bin,post_db_hash.helper-functions.Rretained as a 14-line compatibility shim — pre-existing tests source it directly; shim conditionally loads the 4 split modules.
- D3 — Split
api/functions/pubtator-functions.R(1,269 LoC) into 3 focused modules.pubtator-client.R(351 LoC) — BioCJSON API calls + rate limiting (pubtator_rate_limited_call,pubtator_v3_*).pubtator-parser.R(380 LoC) — JSON parsing + 3-approach gene-symbol computation (pubtator_parse_biocjson,compute_pubtator_gene_symbols, flatteners,generate_query_hash).pubtator-functions.R(548 LoC orchestrator) —pubtator_db_update()(sync) +pubtator_db_update_async()(mirai workers).
- D4 — Deleted
api/functions/legacy-wrappers.R(630 LoC). All 10 wrapper functions migrated to their natural service layer homes with their original names preserved (endpoint handlers and Phase C test sandboxes reference them by name):put_post_db_review,put_post_db_pub_con,put_post_db_phen_con,put_post_db_var_ont_con→api/services/review-service.R.put_post_db_status→api/services/status-service.R.post_db_entity,put_db_entity_deactivation→api/services/entity-service.R.put_db_review_approve,put_db_status_approve→api/services/approval-service.R.new_publication→ already existed inpublication-functions.R; legacy version removed.- All migrated wrapper functions use
logger::log_*namespacing for consistency with the rest of the service files.
- D5 — Pagination sweep on 14 GET endpoints across 7 endpoint files. New
paginate_offset()helper inapi/functions/pagination-helpers.Rreturns a standardized{data, links, meta}envelope with input validation (limitclamped to[1, 500],offset >= 0) and safe URL-separator handling forlinks.next.- Paginated:
backup/list,llm_admin/cache/summaries,llm_admin/logs,re_review/assignment_table, all 4search/*routes,variant/correlation,variant/count,panels/options,comparisons/options,comparisons/upset,comparisons/similarity. backup/listpreserves legacy top-leveltotal/page/page_sizefields for backward compatibility with pre-existing callers while addinglinks/limit/offsetfor the new contract.about/{draft,published}andhash/createintentionally untouched (single-item/non-list endpoints).- New
api/tests/testthat/test-pagination-contract.R(332 LoC, 26 tests) — net-new contract surface allowed by the test gate; validatespaginate_offset()correctness plus static signature extraction confirming all 14 target handlers acceptlimit/offset.
- Paginated:
api/start_sysndd_api.R— Source block wrapped in# --- function source list (v11.0) ---/# --- end source list ---markers.legacy-wrappers.Randhelper-functions.Rsource lines removed/adjusted; new split module source lines added in alphabetical order.everywhere({...})mirai worker block updated to match.
- Docker container restart smoke — mirai workers load the refactored function set without
could not find functionerrors. - All 5 unit PRs (D1–D5) green on CI:
Lint R API,Test R API,Smoke Test (prod stack),make doctor,Detect Changes. test-endpoint-backup.R(76/76),test-endpoint-search.R,test-endpoint-variant.R,test-pagination-contract.Rall green in a live Docker container run.- File size gates: all new files under plan targets except the documented
response-helpers.Randllm-service.Rorchestrator exceptions.
- D6 (extract-bootstrap) — splits
api/start_sysndd_api.Rintoapi/bootstrap/modules. Follows in a sequenced PR after this one lands. - Missing
base_urlon severalpaginate_offset()call sites meanslinks.nextdrops active query params; flagged by Copilot, deferred to avoid expanding Phase D scope. - Weak
post_db_entity()missing-field detection (NULL/NA values) — pre-existing behavior, preserved intentionally by D4 migration.
0.11.6 — 2026-04-12
Phase C of the v11.0 test foundation initiative — the Tier B safety net that Phase D/E rewrites depend on. 11 new test files landed across 6 view functional specs, 2 composable spec pairs, and 3 R endpoint test batches, plus the default-on transaction rollback audit across every pre-existing test-integration-*.R file. No runtime code changed; this is exclusively test authoring plus narrowly-scoped B1 MSW drift fixes uncovered by Checkpoint #2's batch review.
- C1–C6 — View functional specs. Six new
*.spec.tsfiles against unchanged Vue view source, each with a happy path, an error path, and a concreteit.todo(...)handshake that Phase E rewriting agents will turn into passing assertions.app/src/views/curate/ApproveReview.spec.ts(C1, 487 LoC, 3 tests + 1 todo). Routed-axios wrapper passes C1-scoped endpoints through to real MSW while short-circuiting 4 unrelated onMounted GETs that aren't in the B1 table. Bonus "handlers probe"it()block asserts all 5 locked B1 review handlers return their 2xx shapes — durable proof that B1 remains wired even as Phase E5's rewrite changes which code paths invoke them. Lockedit.todo: "verify the correct approver role appears in the audit trail" (E5 handshake).app/src/views/review/Review.spec.ts(C2, 563 LoC, 2 tests + 1 todo). Module-levelvi.mock('axios')for the classification wizard flow sinceReview.vuecalls/api/re_review/table,/api/list/*, and/api/entity?filter=…— none of which are in the B1 table. Error path assertsisFormValid.value === false+ no PUT fires when synopsis is empty (semantic substitution for the plan's literal "Save button disabled", since the view's:disabledbinding is not wired on unchanged source). Lockedit.todo: "verify the step-indicator state after a back-navigation".app/src/views/curate/ApproveStatus.spec.ts(C3, 466 LoC, 2 tests + 1 todo). Uses the real@/plugins/axiosinstance so the 401 response interceptor fires live; error path assertsrouter.push({ path: '/Login', query: { redirect: '/curate/approve-status' } }), auth header cleared, andlocalStorage.tokenwiped on stale token. Lockedit.todo: "verify the combined status/review handling — hook for E6 convergence" (E6 handshake).app/src/views/curate/ModifyEntity.spec.ts(C4, 320 LoC, 2 tests + 1 todo). Component-levelmocks: { axios }mirrors the existingModifyEntity.a11y.spec.tspattern. Happy path asserts the realrename_json.entitywire shape (which is what surfaced the B1POST /api/entity/renamehandler drift — see Fixed below). Error path asserts 409 on duplicate entity creation. Lockedit.todo: "verify unsaved-changes warning on navigation".app/src/views/admin/ManageAnnotations.spec.ts(C5, 564 LoC, 2 tests + 1 todo). Closure-counter polling via a singleserver.use(http.get('/api/jobs/:job_id/status', …))that returns different payloads on successive polls, driven byvi.useFakeTimers({ toFake: ['setInterval', 'clearInterval'] })+advanceTimersByTimeAsync(3000)— precise fake-timer scope keeps axios/MSW/Vue microtasks real. Error path uses the Phase 76status="blocked"safeguard pattern (not a job-cancel endpoint, which does not exist in the API). Lockedit.todo: "verify the force-apply flow fires PUT /api/admin/force_apply_ontology with the correct blocked_job_id" (E4 handshake).app/src/views/admin/ManageUser.spec.ts(C6, 317 LoC, 2 tests + 1 todo). Canonical MSW pattern for future Phase D/E view specs: real@/plugins/axioswired viamocks: { axios, $http: axios }+vi.stubEnv('VITE_API_URL', '')+ seededlocalStorage.token+ minimalGenericTablestub exposingdata-test="cell-user-role". Line 1558 ofManageUser.vueconfirmed asPUT /api/user/updatewith body envelope{ user_details: … }. Error path asserts the localusersarray still showsAdministratorafter a 403 demote-last-admin rejection — prevents optimistic-drop regression. Lockedit.todo: "verify the search-and-filter state persists across role edits and user_role bulk assignments via POST /api/user/bulk_assign_role".
- C10 — useAsyncJob + useEntityForm composable specs. Two new spec files (
app/src/composables/useAsyncJob.spec.ts,useEntityForm.spec.ts) totalling 48 tests. Pins the composables' behavior before Phase E4/E5 consume them.useAsyncJobtests drive the submit→poll→complete, submit→poll→blocked, and submit→poll→error state transitions viaserver.use()+ fake timers; theblockedtest pins the current "composable keeps polling because onlycompleted/failedare terminal" behavior with an inline comment flagging that Phase E4 will tighten it — loud test-diff signal instead of silent drift.useEntityFormhas no HTTP calls (it's pure form state); the contract test forwardsgetFormSnapshot()through aserver.usewrapper aroundPOST /api/entity/createto document the camelCase→snake_case field-name mapping (geneId → hgnc_id) that the host view is expected to perform. - C11 — useTableData + useTableMethods composable specs. Two new spec files (
app/src/composables/useTableData.spec.ts,useTableMethods.spec.ts) totalling 57 tests.useTableDatais a pure reactive state factory (zero HTTP), so no MSW handlers are consumed — the spec asserts refs and computeds directly.useTableMethodsdelegates re-fetches to an injectedloadDatacallback; the spec over-covers the plan minimum with coverage forcopyLinkToClipboard,requestExcel(happy + error + missing-dep no-op),filtered/removeFilters/removeSearchincluding the ref-wrapped filter branch and the missing-anysilent-no-op branch, the URLhistory.replaceStateside effect, and thetruncate/normalizerhelpers. Source-type oddity noted:sortDescis declaredComputedRef<boolean>inTableDataStatebut implemented as a writable computed — the spec casts toWritableComputedRef<boolean>to exercise the documented setter; this is flagged as a future cleanup, not fixed in Phase C. - C7 — Read endpoint testthat batch. Four new
test-endpoint-*.Rfiles covering every HTTP method per route in the read-only endpoint files (exit criterion #5 locked). Each test uses a file-local static handler-shape extraction pattern: parse the endpoint file, extract the anonymousfunction(req, res, …)literal via a decorator regex,eval()into a sandbox env with stubs forrequire_role,pool, repo functions — then assert formals + body text against expected helper references. Mirrors the Phase Atest-endpoint-auth.Rapproach and works without a running plumber server.api/tests/testthat/test-endpoint-search.R— 4@getroutes insearch_endpoints.R× 2 blocks (happy + 404/empty) = 8 test_that blocks.api/tests/testthat/test-endpoint-list.R— 4@getroutes inlist_endpoints.R× 2 = 8 blocks.api/tests/testthat/test-endpoint-statistics.R— 10@getroutes instatistics_endpoints.R× 2 = 20 blocks (the biggest read-batch file).api/tests/testthat/test-endpoint-ontology.R— 2@get+ 1@putroute inontology_endpoints.R= 6 blocks. The@putis included per the file-scope rule of exit criterion #5 even though it's a write route living in a read-only-batch file.
- C8 — Write endpoint testthat batch. Four new test files with 60 test_that blocks across 20 route × method combos, each wrapped in
with_test_db_transaction(). Per exit criterion #5, each combo has happy + validation + permission blocks. For write routes the tests extract handler literals into a sandbox (same pattern as C7); for read-only routes living in these files the "permission" block asserts the absence of arequire_role()call in the handler body blob — a public-read guarantee by construction.api/tests/testthat/test-endpoint-review.R— 8 routes × 3 = 24 blocks coveringreview_endpoints.R(largest write file).api/tests/testthat/test-endpoint-status.R— 6 routes × 3 = 18 blocks.api/tests/testthat/test-endpoint-phenotype.R— 3 routes × 3 = 9 blocks.api/tests/testthat/test-endpoint-variant.R— 3 routes × 3 = 9 blocks.
- C9 — Admin endpoint testthat batch. Two new test files with 27 test_that blocks.
test-endpoint-backup.R(22 blocks across 5 routes:@get /list,@post /create,@post /restore,@get /download/<filename>,@delete /delete/<filename>) — the download happy-path useswithr::local_tempdir()with real bytes and stubsfile/file.info/readBin/file.existsto route reads through the fixture without touching the handler's hard-coded/backup/path.test-endpoint-hash.R(5 blocks across 1@post createroute) tests the hash endpoint with default and custom endpoint forwarding. Thecreate_jobstub inmake_backup_sandboxnever invokesexecutor_fn, cleanly bypassing the mirai-daemon-sourcedexecute_mysqldump/execute_restorepath.
- R1 — B1 handler wire-shape drifts (3 items) found by Checkpoint #2 batch review. The reviewer ran
curlagainst the live dev API for 3 B1 handlers and confirmed drifts that would crash any view going through the real endpoint:GET /api/review/:id— R/Plumber returns a 1-row array for single-row queries, not a bare object.ApproveReview.vue'sloadReviewInfoindexesresponse.data[0].synopsis. Handler now returns[reviewByIdOk]. Smoke test athandlers.spec.ts:257and C1's handlers-probeit()block updated to assert the array shape and index[0].review_id.GET /api/status/:id— same R/Plumber 1-row-array convention.ApproveStatus.vue'sloadStatusInfoindexesresponse.data[0].category_id. Handler now returns[statusByIdOk]. C3's per-testserver.useoverride (which already worked around this viaHttpResponse.json([statusByIdOk])) becomes redundant but remains correct.POST /api/entity/rename— the real endpoint readsreq$argsBody$rename_json$entity$entity_id(entity_endpoints.R:408-419). The old handler checkedbody.sysndd_id/body.new_symbol(flat shape) which don't exist on the real wire — every legitimate request would 400 against the B1 default. New validation accepts the real envelope{ rename_json: { entity: { entity_id, … } } }and retains the legacy flat shape for backwards compatibility. C4's component-level axios mock is unaffected.
- R2 — Default-on rollback audit Q5 gap. C8's rollback audit headers on
test-integration-async.Randtest-integration-llm-endpoints.Rused prose documentation of the HTTP-only exemption but the Layer B grep inscripts/verify-test-gate.shonly accepted the literalskip_if_no_test_db()call paired with an exempt keyword. Fix: widened the grep to also acceptskip_if_api_not_running|skip_if_no_api(the skip helpers these HTTP-only tests actually use — they legitimately skip based on server reachability, not DB availability, and have no client-side transaction to roll back), and widened the exempt-keyword set to includehttp-onlyandread-only. Also widened the Layer A exemption branch prefix fromv11.0/phase-c/test-endpoint-*tov11.0/phase-c/*so thev11.0/phase-c/combinedintegration branch gets the same audit exemption as the per-unit branches. The self-test harness atscripts/tests/test-verify-test-gate.shstays green at 8/8. - R3 — C8/C9 test-code bugs caught by CI. The first push of the C8 (#247) and C9 (#244) PRs failed
Test R APIin CI:test-endpoint-backup.R:493/505/519— theGET /download/<filename>handler error branches callres$serializer <- serializer_json()to switch from@serializer octetto JSON for 4xx responses.serializer_json()is a Plumber function only available when Plumber is loaded as a package, not when handlers are extracted as literal function objects into a sandbox env. Fix: stubserializer_json <- function(...) identityinmake_backup_sandbox().test-endpoint-review.R:226/294— the@post /createand@put /updatehandlers aggregate service-function statuses through a tibble pipeline (review_endpoints.R:319-328,372-381) that callsunique()beforemutate(status = max(status)). When services return distinct messages (e.g. "OK. Review stored." and "OK. Skipped."), the result list'sstatusfield is a length-N vector, not a scalar. Fix:expect_true(all(result$status == 200L))instead ofexpect_equal(result$status, 200), plusexpect_equal(res$status, 200L)for the HTTP-level status.
- Coverage ratchet bumped from 6/4/4/6 to 13/9/12/13 (statements/branches/functions/lines). 11 new spec files pushed the measured coverage from Phase B's floor to
13.22%statements /12.49%branches /9.68%functions /13.66%lines. The new threshold is pinned at the rounded-DOWN measured floor so the ratchet never flaps if a small future refactor shifts the denominator by a fraction. Per Phase C dispatch brief: the plan's stale 45→55 bump is discarded — the real floor is whattest:coverageactually produces, withtest-utils/excluded from the denominator. Inline rationale inapp/vitest.config.ts. scripts/verify-test-gate.sh— Phase Ctest-integration-*.Rrollback audit exemption (see Fixed R2 above) plus a narrow carve-out forapp/src/test-utils/mocks/**/*.spec.tsonv11.0/phase-c/*branches. Thetest-utils/mocksexemption lets B1 smoke-test assertions be updated in lockstep with B1 handler shape fixes (e.g.handlers.spec.ts:257reflecting thereviewByIdOkarray wrap) without the gate rejecting the edit as a pre-existing-spec-file modification. Scope is intentionally narrow so it can't cover up view-spec tautology regressions.
- Bumped
app/package.jsonandapi/version_spec.jsonto0.11.6. - Phase C work was developed across 11 parallel git worktrees (
v11.0/phase-c/*) off Phase-B-merged master (3efce3ae) and combined into a single PR via thev11.0/phase-c/combinedbranch (matches Phase A'sv11.0/phase-a/combinedand Phase B's combined-branch pattern). 15 commits cherry-picked across the 11 units — 8 single-commit view/composable branches, C7's 2-commit read-batch, C8's 3 non-gate commits (with C8'sscripts/verify-test-gate.shedit dropped in favor of C7's strict superset), and C9's 2 non-gate commits (same drop rule). The 3-wayscripts/verify-test-gate.shconflict flagged by the batch reviewer was resolved by keeping C7's most-permissive exemption as the single source of truth on the combined branch. - Checkpoint #2 of 3 — the most important checkpoint in the v11.0 milestone — was executed as a single focused batch review across all 11 PRs via
superpowers:code-reviewer. The reviewer answered the 5 locked questions from.planning/_archive/legacy-plans/v11.0/phase-c.md§7:- Q1 Tautology check: PASS — all 6 view specs assert on real view behavior (reactive data, composable spies, route pushes, toast spies, computed signals), not on mock return shapes.
- Q2 Handshake check: PASS — all 6 locked
it.todostrings present verbatim; each is concrete enough for an E-phase rewriting agent to turn into a passing assertion. - Q3 MSW shape sanity: FAIL — 3/3 random-sampled handlers drifted from the real API wire shape. All 3 drifts fixed in R1 above.
- Q4 Exit criterion #5 scope lock: PASS — every route × method in C7 and C8's target files has the required blocks. C9's admin files are covered per the plan's per-file scope rule.
- Q5 Default-on rollback: FAIL — C8's rollback audit headers on async and llm-endpoints used prose documentation but the Layer B grep required literal
skip_if_no_test_db(). Fixed in R2 above.
- The B1 shape drifts that were NOT closed by R1 (they aren't Phase-D-blocking because every affected Phase C spec already has working local workarounds) are scheduled for a reinforcing Phase B worktree before Phase D opens:
- Add
GET /api/entitylist handler supporting?filter=equals(entity_id,<id>)for ModifyEntity + Review view use cases. - Add
GET /api/list/status(?tree=true),/api/list/phenotype,/api/list/variation_ontologyhandlers. - Add
GET /api/re_review/tablehandler. - Decide per ManageAnnotations aux endpoint (6+ paths) whether to promote C5's per-test
installAuxHandlers()into B1 or keep them test-local.
- Add
- The
scripts/verify-test-gate.shREPO_ROOT false-green flagged by C6 (hard-codesDEFAULT_REPO_ROOT=$(cd "$SCRIPT_DIR/.." && pwd)which can checkmasterinstead of the worktree branch when invoked by absolute path) is a known follow-up to be fixed by the reinforcing worktree along with the B1 drifts. Empirically verified: when the script is invoked from the worktree by relative path (which is what every Phase C agent did), SCRIPT_DIR resolves to the worktree's ownscripts/and the check runs against the correct branch. The false-green is a latent risk only for future scripts that call the gate by absolute path.
- Same host-env constraint as 0.11.4 and 0.11.5:
make ci-localstill fails at the R lint/test steps on Ubuntu 25.10 "questing" hosts running Conda/miniforge R. Phase C's R endpoint tests were verified via CI onubuntu-latest, which is the authoritative baseline. See the "Host-Env Workaround" section ofCLAUDE.md. - The
useTableData.sortDescwritable-computed vsComputedRef<boolean>type declaration mismatch noted in C11's spec is a future cleanup, not fixed in Phase C (Phase C rule: no source modifications).
- PR: #237 — C4 test-view-modify-entity (individual, superseded by combined)
- PR: #238 — C2 test-view-review (individual, superseded by combined)
- PR: #239 — C5 test-view-manage-annotations (individual, superseded by combined)
- PR: #240 — C1 test-view-approve-review (individual, superseded by combined)
- PR: #241 — C11 test-composables-table (individual, superseded by combined)
- PR: #242 — C6 test-view-manage-user (individual, superseded by combined)
- PR: #243 — C10 test-composables-async-form (individual, superseded by combined)
- PR: #244 — C9 test-endpoint-admin-batch (individual, superseded by combined)
- PR: #245 — C3 test-view-approve-status (individual, superseded by combined)
- PR: #246 — C7 test-endpoint-read-batch (individual, superseded by combined)
- PR: #247 — C8 test-endpoint-write-batch (individual, superseded by combined)
- Plan:
.planning/_archive/legacy-plans/v11.0/phase-c.md - Spec:
.planning/superpowers/specs/2026-04-11-v11.0-test-foundation-design.md§3 Phase C
0.11.5 — 2026-04-11
Phase B of the v11.0 test foundation initiative — Tier A test infrastructure that unblocks every Phase C / D / E unit. All 5 units (B1–B5) landed as one combined release. No runtime code changed; this is exclusively dev/test infrastructure (MSW handlers, httptest2 fixtures, CI jobs, test helpers, verify-test-gate logic). Patch bump per SemVer.
- B1 — MSW handler expansion (app/src/test-utils/mocks/). The vitest MSW layer now covers every handler in the locked Phase B.B1 table: 38 handlers across 6 view families (Auth, User admin, Review workflow, Status workflow, Entity curation, Annotation jobs). Every handler has a 2xx happy path and at least one 4xx branch distinguishable by request shape, and every handler carries an OpenAPI-path comment above it.
- New fixture modules under
app/src/test-utils/mocks/data/:auth.ts,users.ts,reviews.ts,statuses.ts,entities.ts,jobs.ts(split by response family, each under 300 LoC). - New smoke spec
app/src/test-utils/mocks/handlers.spec.ts— 77 assertions, one 2xx and one 4xx case per handler, catches handler drift on first run. - New shell script
scripts/verify-msw-against-openapi.sh— greps every handler path against the real@get/@post/@put/@deleteannotations inapi/endpoints/*.Rand reports drift. Wired intomake lint-app. - New
scripts/msw-openapi-exceptions.txt— whitelists 4 entries where the locked spec table points at endpoints that don't exist (yet) on master. Each entry is a spec-bug flag for Phase C to resolve, not a handler bug:PUT /api/user/delete(real annotation is@delete delete),PUT /api/review/approve/all(no bulk route),PUT /api/status/approve/all(no bulk route),GET /api/entity/:sysndd_id(no bare getter — only sub-path routes). The verify script exits non-zero on any unlisted drift.
- New fixture modules under
- B2 — real PubMed/PubTator httptest2 fixtures. Replaced the previously empty
api/tests/testthat/fixtures/{pubmed,pubtator}/directories with 6 real captures (3 pubmed + 3 pubtator, 32.7 KB total), recorded via a newmake refresh-fixturestarget against the live NCBI eUtils and PubTator3 BioCJSON APIs on 2026-04-11. Captured viahttptest2::save_response(..., simplify = FALSE)to preserve the fullhttr2_responseobject; not handcrafted JSON.- New helper
api/tests/testthat/helper-fixtures.R—skip_if_no_fixtures(subdir)fails loudly on missing/empty fixture directories (bothtestthat::fail()andstop(), with an actionable message pointing atmake refresh-fixtures)..gitkeep-only directories are treated as missing. Per spec §4.4 rule 1: the point is to make the silent-skip failure mode impossible to miss. - New
api/tests/testthat/fixtures/README.md— documents every committed fixture with filename, recording date, API version, and exact capture command. - New
make refresh-fixturestarget (disjoint section from A7/A4/A6 Makefile edits) — invokes the capture commands against live APIs when explicitly run; not invoked frommake ci-local. test-external-pubmed.Randtest-external-pubtator.Rnow callskip_if_no_fixtures()at the firsttest_that()of each file.
- New helper
- B3 — skip-slow-wiring. Wired
skip_if_not_slow_tests()(previously defined inhelper-skip.Rbut never called) into 22test_that()blocks across 4 audited files that actually hit Mailpit or live external APIs:test-integration-email.R(5 blocks),test-external-pubtator.R(3),test-e2e-user-lifecycle.R(11),test-external-pubmed.R(3). The other 4 files flagged by the audit grep (test-unit-publication-functions.R,test-unit-pubtator-parse.R,test-unit-genereviews-functions.R,test-unit-pubtator-functions.R) were classified MOCK — they contain the search terms only in comments, string assertions, or mocked bindings — and left untouched.- New
slow-tests-nightlyCI job in.github/workflows/ci.yml— cron0 3 * * *plusworkflow_dispatch, runsRUN_SLOW_TESTS=true make test-api-fullwith MySQL + Mailpit service containers. Correctly skipped on normal PR runs (verified: the pull_request run'sSlow Tests (nightly)resolves toskippingwhileTest R APIruns green in ~23 min without Mailpit). Bannered as# ===== Phase B B3: slow-tests-nightly =====to make the combined-merge with B4'ssmoke-testjob trivial.
- New
- B4 — CI smoke test + real verify-test-gate.sh. New
smoke-testCI job in.github/workflows/ci.yml(triggered onpushandpull_request) runsscripts/ci-smoke.shwhich wrapsmake preflightplus acurl -fretry loop against/api/health/ready. Bannered as# ===== Phase B B4: smoke-test =====(disjoint from B3's nightly block).ci-successgates onsmoke-test(but notslow-tests-nightly, which is schedule-only).- New
scripts/ci-smoke.sh— boots the full prod stack viamake preflightand verifies readiness. - Replaced the A6
scripts/verify-test-gate.shstub (2-line echo) with 121 lines of real logic. Protects Phase D / Phase E PRs from silently mutating pre-existing test files to "pin" them to whatever the refactor produced. Rule summary: new*.spec.ts/test-*.Rfiles are allowed; modifications to pre-existing spec/test files are rejected unless one of two branch-gated exemptions applies — (a) addingskip_if_not_slow_tests()onv11.0/phase-b/*only (for B3), or (b) replacingSys.sleep(N)withwait_for(..., timeout = N)onv11.0/phase-b/*only (for B5).--extendedmode also greps everyapi/tests/testthat/test-integration-*.Rfile and asserts it opens withwith_test_db_transactionor a documentedskip_if_no_test_db()exemption. - New bash unit-test harness
scripts/tests/test-verify-test-gate.sh— 7 cases (new-spec-allowed, pre-existing-spec-rejected, phase-b skip exemption allowed, phase-b wait_for exemption allowed, phase-b exemption does NOT leak into phase-d, extended-mode rejects integration test missing rollback, extended-mode accepts well-formed repo). All 7 cases pass. - New
make verify-gatetarget wires the harness into CI without an R dependency.
- New
- B5 — Sys.sleep eviction. Evicted every real
Sys.sleep(N)from the R test suite (4 call sites intest-e2e-user-lifecycle.Rat lines 181/215/323/539, 1 inhelper-mailpit.Rat line 116 — the otherSys.sleepoccurrences intest-unit-*.Raremockery::stubbindings ortest-publication-refresh.Rcomments and were correctly left untouched).- New helper
api/tests/testthat/helper-wait.R(297 lines) — defineswait_for(condition, timeout, label)(event-based polling, fails loudly on timeout with a diagnostic including last observed state) and a siblingwait_stable(probe, duration, label)for the negative-assertion case ("no change should occur for N seconds"; fails immediately on any change, strictly faster than a fixed sleep + single check on failure). helper-mailpit.R::mailpit_wait_for_messagerefactored to delegate towait_for()— no more internalSys.sleeppolling.- The 4 e2e call sites were all "no email should arrive" negative assertions, now using
wait_stable(mailpit_message_count, N)with a baseline captured just before the action. Thewait_stableapproach fails immediately on any unexpected email rather than waiting the full sleep window. - 10-iteration flake check passed 10/10 in the prod sysndd-api Docker container (helper-wait self-tests: 10/10, 190/190 assertions; test-e2e-user-lifecycle.R load + dispatch: 10/10, all 11
test_thatblocks reachskip_if_no_mailpit()/skip_if_no_api()cleanly).
- New helper
app/vitest.setup.ts— MSWonUnhandledRequestflipped from'warn'to'error'. Every unmocked request now hard-fails the test, making any handler gap impossible to miss. Acceptance criterion: no pre-existing vitest was left failing because of this switch (full suite of 321 tests still green on the combined branch).app/vitest.config.ts— coverage thresholds pinned at the current measured floor (lines: 6,functions: 4,branches: 4,statements: 6). B1 originally "bumped 40 → 45" but the actual coverage is only 4–7% becausetest-utils/is excluded from the coverage denominator — the original 40 threshold was decorative since no CI job runsnpm run test:coverage. Thresholds now form a ratchet that future phases must raise as specs land; see the inline comment inapp/vitest.config.tsfor the rule and rationale.
- Bumped
app/package.jsonandapi/version_spec.jsonto0.11.5. - Phase B work was developed across 5 parallel git worktrees (
v11.0/phase-b/*) off Phase-A-merged master (db18cb51) and combined into a single PR for review, following the Phase A pattern. B5 merged first on the test-file conflicts (per the tiebreaker rule); B3 and B4's disjointci.ymljob blocks merged cleanly with both banners intact;ci-success.needscorrectly unions to includesmoke-test(PR-gating) but notslow-tests-nightly(schedule-only). - End-to-end verification on the combined branch was done via a Playwright monkey-walk against the full dev stack (traefik + api + app + mysql + mailpit) bound to the combined worktree. Walked 13 routes including unauth public views (Genes, Entities, Phenotypes, Panels, PublicationsNDD, Gene detail, About), the post-A1
POST /api/auth/authenticatelogin flow end-to-end against the live API (not a mock), and 4 authed views covering B1's mocked handler families (/,/ManageUser,/ManageAnnotations,/ApproveReview,/ApproveStatus). Zero Phase-B-introduced regressions; the only console errors encountered were (a) the expected 401 on/api/auth/signinfor unauthenticated visitors and (b) two pre-existing 404s on/api/external/{mgi,rgd}/phenotypes/A2ML1that reflect a data gap in the upstream MGI/RGD records, unrelated to Phase B. - Per-endpoint sanity-check (§7 of
.planning/_archive/legacy-plans/v11.0/phase-b.md): curled 4 handler-table endpoints against the live API on the combined worktree and confirmed B1's mock shapes are faithful —GET /api/status/1returns a full status record matching the mock shape;POST /api/auth/authenticatewith bad creds returns HTTP 400 with the documented "Please provide valid username and password." body (matches B1's 4xx branch);GET /api/user/role_listandGET /api/jobs/historyreturn 403 without a JWT (consistent with therequire_authmiddleware behaviour B1 assumes).
The first push of this PR surfaced six actionable items from the automated Copilot review plus one CI failure (smoke-test could not build) plus one misconfigured gate (vitest coverage thresholds). All are fixed in the final combined branch before merge — commit chore(phase-b/combined): fix Copilot review + codecov + CI smoke-test:
- Copilot #1 — Makefile
.PHONYgap.verify-gateadded to the.PHONYdeclaration so a stray file of that name cannot shadow the target. - Copilot #2 —
%||%inapi/scripts/capture-external-fixtures.R. Replaced the rlang-only operator with a base-R fallback that resolves the script path viasys.frame(1)$ofile, thencommandArgs(trailingOnly = FALSE)--file=, then".". The script no longer has an implicit rlang dependency. - Copilot #3 —
helper-wait.R::is_truthywas too permissive. The oldis.atomic(v) -> TRUEbranch would return early on0,NA,"",FALSE, defeating the "wait until ready" semantics for any probe that uses a count-or-zero sentinel. Tightened to treat onlyisTRUE()logicals, non-empty lists, and non-empty data frames as truthy. A new 14-assertion test case (wait_for does NOT treat atomic 0/NA/empty-string as truthy) plus a 4-assertion test for non-empty list/data.frame pin the new semantics — verified 9/9 blocks / 35/35 assertions green in the sysndd-api prod container. - Copilot #4 — Bash 4+ in
verify-msw-against-openapi.sh. The associative array (declare -A) is replaced with a portable indexed array +is_exception()lookup so the script runs on Bash 3.2 — the version still shipped as/bin/bashon macOS. A defensiveBASH_VERSINFOcheck surfaces a friendly error on anything older. - Copilot #5 — CI smoke-test failure. The first PR-236 push made the smoke-test CI job fail at the Docker build step (
"/config.yml": not found) becauseapi/config.ymlis gitignored on dev machines but the prod Dockerfile doesCOPY config.yml config.yml. Fix: committedapi/config.yml.examplewith CI-safe dummy values (structurally identical to the real config, credentials aligned with.env.example's placeholders so the dummy API container can actually reach the dummy MySQL user) and extendedscripts/ci-smoke.shwith aseed_from_templatestep that copiesapi/config.yml.example → api/config.ymland.env.example → .envwhen either is missing. Idempotent — it does not overwrite real dev secrets. - Copilot #6 —
verify-test-gate.shSys.sleep exemption was too permissive. The old exemption accepted any diff on av11.0/phase-b/*branch as long as it contained at least one removedSys.sleep(line and one addedwait_for(...)line — unrelated line changes could slip through. Tightened to a whitelist: every added/removed non-blank, non-comment line must match a narrow set of tokens (wait_for(,wait_stable(, named kwargs, closing paren, mailpit probe helpers, baseline assignments). A new harness case (phase-b exemption rejects unrelated edits paired with Sys.sleep->wait_for) pins the tightened behaviour; all 8 harness cases now pass. - Codecov / vitest coverage thresholds — pinned at realistic floor. See
Changedabove. - Self-review S1 —
ERROR_SENTINELSconstants block. Added a typed const export at the top ofapp/src/test-utils/mocks/handlers.tsdocumenting the path-param / query / header sentinels that trigger 4xx branches. Existing handlers and specs keep their literals for now; future handlers/specs should import fromERROR_SENTINELSso the contract is discoverable.
- Same host-env constraint as 0.11.4:
make ci-localstill fails at the R lint/test steps on Ubuntu 25.10 "questing" hosts running Conda/miniforge R. Phase B's entire R test verification was done via thesysndd-apiDocker container or deferred to CI onubuntu-latest, which is the authoritative baseline. See the "Host-Env Workaround" section ofCLAUDE.mdfor the details. B1flagged 4 drifts where the locked handler table points at endpoints that do not exist on master (whitelisted inscripts/msw-openapi-exceptions.txt). These are spec bugs for Phase C to resolve, not handler bugs — either the handler table needs updating or the missing endpoints need to be added in Phase C / D when the views actually consume them. Seescripts/msw-openapi-exceptions.txtfor the full list with rationale.
- PR: #230 — B3 skip-slow-wiring (individual, superseded by combined)
- PR: #231 — B4 ci-smoke-test (individual, superseded by combined)
- PR: #232 — B2 pubmed-pubtator-fixtures (individual, superseded by combined)
- PR: #233 — B5 sys-sleep-eviction (individual, superseded by combined)
- PR: #234 — B1 msw-handler-expansion (individual, superseded by combined)
- Plan:
.planning/_archive/legacy-plans/v11.0/phase-b.md - Spec:
.planning/superpowers/specs/2026-04-11-v11.0-test-foundation-design.md§3 Phase B
0.11.4 — 2026-04-11
Phase A of the v11.0 test foundation initiative. A1–A7 plus a focused follow-up, landed as one release.
On the first API boot after deploying this version against a database that was previously running 0.11.3 or earlier, the migration runner emits exactly one INFO log line:
[INFO] reconcile_schema_version_renames: rewriting schema_version.filename '008_hgnc_symbol_lookup.sql' -> '018_hgnc_symbol_lookup.sql'
This is the new reconcile_schema_version_renames() step in api/functions/migration-runner.R reconciling the filename rename introduced by A4 (see Changed below). It runs before the pending-migration diff, so the renamed migration is not re-executed.
- No manual DML is required. The reconciliation is idempotent: it is a no-op on every subsequent boot and on any fresh database where
008_hgnc_symbol_lookup.sqlwas never recorded. - What would have happened without it:
migration-runner.R'ssetdiff(migration_files, applied)would have seen018_hgnc_symbol_lookup.sqlas pending and re-executed it.CREATE TABLE IF NOT EXISTSis safe, but the threeINSERT INTO hgnc_symbol_lookupstatements are not idempotent and would have duplicated rows. - Sanity check (optional but recommended):
SELECT COUNT(*) FROM hgnc_symbol_lookup;before and after the deploy — the counts should match exactly. A mismatch means the reconciliation failed and the migration was re-executed. Roll back and investigate. - Fail-fast behavior: if the reconciliation hits a genuine DB error (broken connection, locked
schema_version, etc.), API startup aborts loudly rather than silently proceeding into the main migration loop with an unreconciled state. This is the Risk 5 mitigation agreed during Copilot review — see the module-level doc comment onMIGRATION_RENAMESinapi/functions/migration-runner.R.
Context: Phase A.A4 resolves a duplicate 008_ migration prefix by renaming 008_hgnc_symbol_lookup.sql → 018_hgnc_symbol_lookup.sql. On any deployment that had the old file recorded in schema_version, the filename tracker is now stale. The reconciliation is what makes this deployment-safe. See .planning/_archive/legacy-plans/v11.0/phase-a.md §3 A4 for the full rationale.
- A1 (P0 hotfix): Moved login and password-change credentials out of URL query strings. The previous
GET /api/auth/authenticate?user_name=…&password=…andPUT /api/user/password/update?…shapes leaked secrets into access logs, Traefik logs, and browser history.- New:
POST /api/auth/authenticatewithContent-Type: application/jsonand body{"user_name":"…","password":"…"}. - New:
PUT /api/user/password/updateaccepts a JSON body for the password fields. Handler is dual-mode: the legacy query-string form still works as a transitional fallback and will be removed in a later release (tracked as Phase E.E7 in the v11.0 plan). - Deprecated (still functional in 0.11.4):
GET /api/auth/authenticate. Will be removed alongside the dual-mode password handler in Phase E.E7. app/src/views/LoginView.vueandapp/src/views/UserView.vueswitched to the new POST/PUT shapes.- Middleware
AUTH_ALLOWLISTupdated to include/api/auth/authenticateso the new@posthandler is reachable through therequire_authfilter (the legacy@getonly worked because unauthenticatedGETrequests are forwarded by default). This was caught by end-to-end Playwright testing after the subagent's host-side curl tests missed the interaction with the full Traefik + filter stack.
- New:
- A2:
/api/gene/:symbolno longer corrupts thegnomad_constraintsJSON blob by pipe-splitting it. The repository'sacross(...)call now excludesgnomad_constraintsfromstr_split_fnusing-any_of("gnomad_constraints")(schema-tolerant form). The frontend no longer carries the[0]dereference workaround inGeneView.vue;app/src/types/gene.tsnow types the field asstring | nullwith a JSDoc explanation of why this one field is the scalar exception.
- A7 (already on master, merged in #220, released as part of 0.11.4): One-command developer bootstrap.
make install-dev— idempotent aggregate bootstrap for R (viarenv::restore()) and frontend (vianpm install).make doctor— environment verifier: Docker reachability (soft check), git ≥ 2.5, Node major matchesapp/.nvmrc, R callable, dev packages importable (lintr,styler,testthat,covr,httptest2,callr,mockery). Exit 0 on healthy; exit 1 with a specific diagnostic on any failure.make worktree-setup NAME=<scope>/<unit>— parameterized worktree creation. Createsworktrees/<scope>/<unit>on branchv11.0/<scope>/<unit>from master, withmkdir -pfor the parent directory so nested paths work on a clean clone.app/.nvmrcpins the Node major to match.github/workflows/ci.yml(currently Node 24).- Human-facing
docs/DEVELOPMENT.md(counterpart to the agent-facingCLAUDE.md): six sections covering requirements, quickstart, daily workflow, parallel worktree workflow, common gotchas, and getting help. - Root
CONTRIBUTING.mdwith a minimal TL;DR and a link todocs/DEVELOPMENT.md. api/renv.lockadditions for the 7 declared dev packages (verified via arocker/r-ver:4.5Docker sidecar because the development host runs Conda R on Ubuntu 25.10 "questing", which Posit PPM does not support yet).- New CI job
make doctor (ubuntu-latest)on every PR that touches relevant paths. macOS was tried via colima + homebrew R but hit pre-existing Bioconductor lockfile rot and toolchain issues unrelated to A7 and was removed from the matrix; see the comment header on themake-doctorjob in.github/workflows/ci.ymlfor the full rationale.
- A3:
db/migrations/README.mdrewritten to document the actual runner behavior — advisory lock with 30s timeout, fast-path skip, numbered-prefix convention, forward-only rollback policy, and a cross-reference to the Phase B.B4 CI smoke test. - A4:
scripts/check-migration-prefixes.sh— POSIX shell script that asserts uniqueNNN_migration prefixes acrossdb/migrations/*.sql. Wired intomake lint-api; fails CI on any future collision with a clear diagnostic listing the conflicting files. - A6:
make worktree-prunetarget —git worktree prune -v+git worktree list, safe as a no-op on clean master.scripts/verify-test-gate.shstub (Phase B.B4 will fill in the real test-gate logic). - Follow-up:
reconcile_schema_version_renames()inapi/functions/migration-runner.Rwith an internalMIGRATION_RENAMESmap documenting historical renames (currently the A4 008→018 entry). Runs before the pending-migration diff inrun_migrations(). Fail-fast on DB errors (no silent skip) per Copilot review. 7mockery::stub-based unit tests inapi/tests/testthat/test-unit-migration-runner.Rlock in each state branch: rewrite, idempotent (new-already-present), dedup (both-present), fresh DB, premature-rename (new file not yet on disk), SELECT-error propagation, UPDATE-error propagation. - A1 (tests): New
api/tests/testthat/test-endpoint-auth.R— structural regex assertions and behavior tests for the new POST/PUT handlers. Usesparse()+ source-ref extraction instead ofplumber::pr()because plumber 1.3.2's internal route layout did not match the initial walker; the parse-based form is more portable and runs without plumber installed at test time.
- A4:
db/migrations/008_hgnc_symbol_lookup.sqlrenamed todb/migrations/018_hgnc_symbol_lookup.sql. File content unchanged; the rename resolves the duplicate-prefix issue flagged in the 2026-04-11 codebase review §2. See Upgrade notes above for deploy-time behavior on long-lived databases.
- A5: Empty
api/repository/directory (archaeological debris from an incomplete refactor;api/functions/legacy-wrappers.Ralready covers the repository-layer semantics). No live R code underapi/endpoints,api/functions,api/core,api/services, orapi/start_sysndd_api.Rreferences this directory. - Follow-up: Two
api/repositoryreferences indocker-compose.yml— the volume bind-mount (line 144) and thedevelop.watchsync rule (line ~196). Without this removal,docker compose upanddocker compose watchwould have recreated the empty host-side directory on every invocation, reintroducing the exact state A5 was meant to eliminate.
- Bumped
app/package.jsonandapi/version_spec.jsonto0.11.4. - Tests for the new reconciliation function run entirely offline (
mockery::stubcovers allDBI::dbGetQuery/DBI::dbExecutecall sites). - The Phase A work was developed across 7 parallel git worktrees (
v11.0/phase-a/*) and combined into a single PR (#228) for review. All historical branches have been deleted. The v11.0 plan files under.planning/_archive/legacy-plans/v11.0/describe the parallel-worktree workflow and the intra-phase ownership rules.
make ci-localstill fails at the lint step on Ubuntu 25.10 "questing" hosts running Conda/miniforge R because Posit Package Manager does not yet publish a__linux__/questing/binary repo and Conda R'sldcannot link zlib from source tarballs. Workarounds are documented in the agent-facingCLAUDE.md(gitignored, local memory). CI onubuntu-latestis the authoritative baseline.- The A1 POST handler returns HTTP 500 on a malformed JSON body (e.g. non-JSON text with
Content-Type: application/json) instead of a clean 400. This is plumber's upstream JSON parser dying before the handler's own validation runs. Not a Phase A regression — the prior@getform simply never had this code path. Will be addressed in Phase E.E7 when the auth consolidation lands and the dual-mode handler is removed. - The A4 prefix check script is wired into
make lint-api, but GitHub Actions'changesfilter skips thelint-apijob on PRs that don't touchapi/**. This means a PR that only touchesdb/migrations/might not exercise the prefix check in CI. Phase B.B4's verify-test-gate will close this gap; until then, the script runs locally on everymake lint-apiinvocation.
- PR: #228 — combined Phase A (A1–A6 + follow-up + version bump, 22 commits)
- PR: #220 — Phase A.A7 dev-environment bootstrap (merged first, 10 commits)
- Plan:
.planning/_archive/legacy-plans/v11.0/phase-a.md - Spec:
.planning/superpowers/specs/2026-04-11-v11.0-test-foundation-design.md§3 Phase A, §4.8 local developer environment - Review:
.planning/reviews/2026-04-11-codebase-review.md§2 (duplicate prefix), §3 (empty repository) - Follow-up todo:
.planning/todos/pending/refresh-stale-bioconductor-pins-in-renv-lock.md(pre-existing lockfile rot surfaced by A7's CI matrix; deferred)
0.11.3 — 2026-04-09
- Dependency security updates (bulk bump of production-minor-patch group).
- Dev server fix: allow Docker proxy hosts in Vite 7 + Traefik routing.
Earlier history is available via git log --grep="bump version" on master. This CHANGELOG starts documenting the project at 0.11.3.