From R Package Spaghetti to DRAN: How Codex Helped Tame drwateR

A systematic audit and refactor of the DRWATER core R packages, covering dependencies, portability, security boundaries, documentation, versioning, NEWS, tags, and DRAN publication.
Software
R
DRAN
Author

Ming Su

Published

Sep 6, 2026

It started as a software archaeology site

Some software landscapes look like projects from a distance and like ruins up close. drwateR and its core dependencies had plenty of useful functionality, but also a long memory: README files that had become family heirlooms, platform assumptions hiding in command calls, and a release process relying on tribal knowledge such as “remember to delete the old tar.gz first.”

This was not treated as a request for a fresh coat of paint. The goal was to make the ecosystem maintainable as real R software: inspectable, testable, portable, documented, versioned, taggable, and ready for DRAN, without requiring the next maintainer to summon the original author by ritual.

The rescue route from package spaghetti to DRAN.

Scope before shovels

The maintenance boundary was made explicit. The current core set contains ten DRWATER-related packages:

  • drwateR: the ecosystem entry point and shared utilities, including update checks.
  • dfeR: data-format and data-processing helpers.
  • dateR: date and time utilities.
  • langeR: language and internationalisation helpers.
  • uniteR: units, dimensions, and consistent representation.
  • cctdb: database access and related data interfaces.
  • cctda: algae and aquatic-environment data analysis.
  • dwfun: general DRWATER functions and analysis helpers.
  • rmdify: R Markdown, Quarto, and complex-markup processing.
  • figeR: figures, graphics, and figure-workflow helpers.

dateR1 and the retired customsetup package were deliberately left out. Clear scope prevents an audit from becoming infinite archaeology.

The audit: from “it runs” to “it can be maintained”

1. Metadata became an API

Each core package now exposes conventional metadata helpers: <pkg>_version(), <pkg>_info(), and <pkg>_lifecycle(). The lifecycle is declared in DESCRIPTION, so package identity and maintenance status are read from the package rather than guessed from a README, a script, or someone’s memory.

2. Security before convenience

Database code was reviewed with a strict distinction between SQL values and SQL identifiers. Values belong in parameterised queries; table and column names require explicit validation. Dynamic evaluation is treated as an input-boundary problem, not as a shortcut. “Flexible” should not quietly become “send arbitrary user input to eval(parse()).”

3. macOS, Linux, and Windows are not one operating system

The portability pass covered path construction, executable discovery, temporary directories, platform viewers, and system2() calls. External commands should use Sys.which(), R.home("bin"), and file.path() rather than assuming /bin/sh, .exe, one HOME layout, or one path separator. system2() is not the villain; hard-coding one platform’s command names, arguments, and shell semantics is.

rmdify: the stress test with plot twists

The most entertaining case was rmdify and its clab parser. Real documents do not always keep a marker on one obedient line. A block may span lines and paragraphs, contain nested references and equations, include inline R, or surround a figure chunk. The parser therefore moved to a state-machine approach that tracks bracket depth and fenced code, instead of trusting one clever regular expression and a perfectly behaved document.

Complex clab blocks can now cross paragraphs, and both the legacy and options clab="..." attribute forms are supported. Inline code such as 2 is processed; fenced R/Quarto chunks are preserved and are not executed during parsing. Figure references should use an explicit Quarto label:


::: {.cell}

```{.r .cell-code}
ggsavep("../figures/demo.pdf", loadit = TRUE)
```
:::

That boundary matters: a document transformer may understand code structure, but it should not execute arbitrary code merely because it encountered a code fence.

Release became an actual process

The core packages now use a common Makefile workflow: bump the version, run roxygenisation, generate NEWS, summarise the main changes, apply conservative check gates, commit to main, push the commit, create and push a version tag, and build an exact DRAN index.

One particularly “spaghetti” release trap was also removed. A stale root tarball could be copied instead of the package just built. The workflow now removes old tarballs before building and matches the output against the exact version in DESCRIPTION. The terminal should not say “success” while the repository quietly ships last month’s package.

Each package also received Chinese and English README files, NEWS entries, and a hex logo. A logo does not cure technical debt, but it does remind us that a package should be runnable, discoverable, readable, and usable by humans.

The result: reproducibility, not magic

After this pass, all ten core packages received version updates, checks, commits, pushes, and tags, and were rebuilt into the local DRAN repository. drwateR also exposes a client-side update check:

drwateR::check_drwateR_updates()

It does not upgrade packages behind the user’s back. It reports available updates and leaves the auditable, reversible decision to the user.

An audit is not a declaration that technical debt has gone extinct. Some historical examples and older documentation still deserve cleanup, and the full example gate should be restored incrementally. Honest release notes are more useful than a poster claiming that every corner is perfect: we know what is stable and where the next shovel belongs.

The package spaghetti was not defeated by magic. It was cut into testable, publishable, rollback-friendly pieces. The next answer to “How do we release this package?” is no longer “find the person who wrote it.” It is: read the Makefile, run the checks, read NEWS, and let the version number tell the truth.