# Project map

File-by-file map of the repository, with the direction of every dependency.

**Files: 45** · generated 2026-10-21 · `tests/test_static.sh` verifies the count
against `git ls-files`, so this document cannot go stale silently the way the
auto-generated map of an earlier revision did (it declared 5 files while the
branch shipped 11, in a second language, with a content fingerprint that did not
match its own content).

The count covers the shipped tree: the product, its tests, its documentation and the
`legacy/` archive. The frozen `6.3/` and `6.4/` snapshots, `bin/`, `.compare/`,
`.zwork/` and generated files (logs, lock files, the site list, backups, `dist/`) are
excluded by `.gitignore`.

If you add or remove a file, update the count above and the table below in the
same commit.

## Runtime

| File | Version | Purpose | Depends on |
|---|---|---|---|
| `Bash_WP-CLI_Update.sh` | 8.0.4 | Runs WP-CLI maintenance over every site in the list, each as its owner. Modes (mutating, read-only and diagnostic), configuration layers, locking, logging, timeouts, licence handoff, backups and restore, retries and the circuit breaker, JSON reports and notifications, rendering. | `Find_WP_Senior.sh` (only when the site list is missing and `AUTO_DISCOVER` is on) · `wp` · `runuser`/`sudo`/`su` · `flock`, `timeout` or `perl` (optional) · `gzip`, 
ice`, `ionice`, `curl`, `mail` (optional) |
| `Find_WP_Senior.sh` | 2.1.0 | Scans named web roots for WordPress installations, honours `.no_wp_cli` and exclusions, deduplicates, enriches with metadata, writes the list atomically. | `find`, `sort`, `stat`, `mktemp` |
| `wp-cli-update.conf.example` | — | Every manager setting with its default and the reason it exists. Copy to `/etc/wp-cli-update.conf` or next to the script. | read by `Bash_WP-CLI_Update.sh` |

The two scripts are independent executables: neither sources the other, and the
only coupling is the site-list format (one absolute path per line, no header, no
trailing whitespace) and the `--output` / `--sites` flags.

## Tooling

| File | Version | Purpose | Depends on |
|---|---|---|---|
| `tools/scan-secrets.sh` | 1.1.0 | Looks for credential literals in the working tree and, with `--history`, in every reachable commit. Masks values by default, prints a fingerprint, classifies benign hits, exits 1 under `--strict`. | `git` (only for `--history`) · `sha256sum` or `cksum` |
| `tools/secret-allowlist.txt` | — | Known-benign lines, as `path-glob\|line-substring\|reason`. Never scanned itself, because it quotes the shapes it documents. | read by `tools/scan-secrets.sh` |

## Tests

| File | Purpose | Depends on |
|---|---|---|
| `tests/run_tests.sh` | Runs every suite, prints the environment it found, tallies all suites, exits non-zero on any failure. Accepts suite names as arguments. | `tests/test_*.sh` |
| `ANALYSIS.md` | Audit of the **original** `main` code: every defect with a line citation, the reproduced command-injection proof of concept, what was verified and is *not* a defect, and the residual limitations of the replacement. | `legacy/` |
| `REFACTORING.md` | What v6.2.0 took from each of the five revisions, what it rejected and with which measurement, and the fourteen defects found while integrating. | `ANALYSIS.md`, `tests/` |
| `legacy/Bash_WP-CLI_Update.v5.0.sh` | Byte-exact copy of `main:Bash_WP-CLI_Update.sh`, verified with `git hash-object`. Executed by nothing. Exists so that citations in `ANALYSIS.md` stay checkable without git history and so that rollback does not depend on git. | — |
| `legacy/Find_WP_Senior.v1.01.sh` | Byte-exact copy of `main:Find_WP_Senior.sh`. Same reason. | — |
| `legacy/README.md` | Why the archive exists, and a do-not-fix notice: the known defects are left exactly as they were. | — |
| `tests/test_fleet.sh` | The fleet-wide surface: `--no-user-switch`; `-j N` measured against a sleeping stub (5 s sequential vs 1 s at `-j 5`, 3 s at `-j 2`); counters identical at `-j 1/2/3/5`; console order and log contiguity; a failing site inside a batch; a worker killed with SIGKILL; leaked worker directories; `--backup db\|full` and rotation; an impossible backup destination; plugin-deletion backup and deactivation; `--verify` issues no mutation; `--only-active` and `--exclude-plugins`; `--strict`; JSON Lines shape, ordering and purity; `--list-sites`; `--url`; the configuration layers for the new keys; every shell metacharacter in a config file. | `tests/harness.sh` |
| `tests/harness.sh` | Shared harness: file-backed counters (so a check inside a pipeline still counts), the synthetic WordPress tree, the stub `wp` installation, environment probes, assertion helpers, SKIP handling. | `tests/stub/wp` |
| `tests/stub/wp` | Recording stub of WP-CLI. Logs its own argv, cwd, user, the environment contract and whether a licence arrived (length and checksum only, never the value). Answers `plugin list` in table and JSON. | — |
| `tests/test_static.sh` | `bash -n`, ShellCheck cleanliness, LF endings, executable bits, shebang and mode switches, absence of banned constructs, absence of personal data, documentation present and version-consistent, project map not stale. | `shellcheck` (SKIPs without it) |
| `tests/test_base_contract.sh` | The public surface of the **original** scripts: all ten modes and their short forms, documented options, exit-code contract, `.no_wp_cli`, paths with spaces, the `DOCUMENT_*` environment contract, CRLF lists, and that no scan exceeds the requested roots. | `tests/harness.sh` |
| `tests/test_finder.sh` | Detection rule, exclusions, depth, deduplication, four output formats (JSON and CSV validated with a real parser), atomic write and permission preservation, `--status`, `--skip-existing`, empty-result exit code, stdout/stderr separation. | `tests/harness.sh` |
| `tests/test_manager.sh` | Per-mode argv, `--skip-plugins` and `--allow-root` policies, dry run, four list formats, `--fields`, `--name` filtering, slug resolution, ambiguity refusal, delete confirmation, injection attempts, licence containment, `--check`, `--status`, failure counting, `--fail-on`, timeouts, concurrency, stale locks, config precedence, config rejection, log rotation, colour policy, no writes into the repository. | `tests/harness.sh` |
| `tests/test_secretguard.sh` | Planted values in five name conventions, masking by default, `--strict`, the allowlist, `--history` for a value that was committed and then removed, false-positive classes, `--min-length`, `--quiet`, target arguments. | `tests/harness.sh`, `git` (SKIPs without it) |
| `tests/test_v7.sh` | The 8.0.0 feature set: `--health` (read-only: no dump, no backup, no lock), `--secrets` (names, never values; permissions; stale copies), `--doctor`, `--completion`, `--report` (valid JSON), the change log, retries and their count, the circuit breaker, `--skip-if-no-updates`, plugin/theme policy lists, `--restore-db` (refuses without `--yes`, dumps first), the disk guard, age-based retention, `--url` detection, `--nice`/`--ionice`, config aliases and the new keys. | `tests/harness.sh`, `python3` (optional, validates the report) |

## Documentation and repository hygiene

| File | Purpose |
|---|---|
| `README.md` | Operator documentation: what changed in 8.0.0, safety model, install, quick start, every mode and option, configuration reference, reports and notifications, exit codes, production deployment, tests, upgrade notes. |
| `AGENTS.md` | Install-and-configure instructions for an LLM agent (or a careful human): preconditions, ordered install, configuration rules, verification order, troubleshooting table, absolute do-nots, how to extend the product. |
| `CHANGELOG.md` | Keep a Changelog. User-facing; an internal agent log is not a changelog. |
| `AGENT.md` | Pointer to `AGENTS.md`, kept because older tooling looks for this name. |
| `docs/COMPARISON.md` | The three-way comparison (`main`, `QWEN_Hybrid`, `DeepSeek_Hybrid`) that produced 8.0.0: metrics, feature matrix, what came from where, what was rejected and why, defects found, what all three had forgotten, and the verification record. |
| `SECURITY.md` | Threat model of a root tool, supported versions, how to report, known non-issues, hardening checklist. |
| `package.json` | npm packaging: name, version, `bin` shims, `files` whitelist, `os: linux`, licence. `prepublishOnly` re-checks the syntax of all four shell files. |
| `bin/wp-cli-update`, `bin/wp-find` | The npm shims: resolve their real path through the symlink npm creates, then `exec bash` the script with the same arguments. |
| `install.sh` | Idempotent installer: code, symlinks, log/backup/state directories, config (only if absent), optional systemd units. `--dry-run`, `--uninstall`. |
| `Makefile` | `make test`, `make lint`, `make dist`, `make install`. |
| `.editorconfig` | LF endings and 4-space indent; the two settings that break a shell script silently. |
| `CONTRIBUTING.md` | The four invariants, the fixed order for adding a feature, versioning, commit rules. |
| `docs/README.md` | Documentation index: by document and by task. |
| `docs/PUBLISHING.md` | How a release is cut: version bump, the three artefacts, what the workflow checks, and the npm step that is deliberately manual. |
| `.github/workflows/release.yml` | On a `v*` tag: re-runs the gates, checks that tag and `SCRIPT_VERSION` agree, builds `dist/`, publishes the release with the tarball and `SHA256SUMS`. |
| `.github/workflows/ci.yml` | CI: shellcheck, the full suite on bash 5.x and on the 4.2 floor, documentation and version consistency, and a check that the test run leaves no files behind. |
| `contrib/wp-cli-update.service`, `contrib/wp-cli-update.timer` | A nightly systemd run: nice/ionice, a three-hour ceiling, journal logging, randomised delay. |
| `contrib/cron.example` | The same with plain cron, plus a read-only `--health` probe for monitoring. |
| `contrib/logrotate.conf` | Long-term rotation for the log directory, with `copytruncate` (the manager keeps its log open for the whole run). |
| `PROJECT_MAP.md` | This file. |
| `.gitignore` | Generated and local files: the site list, logs, lock files, key files, backups, `dist/`, the RAGRAF index, the frozen snapshots. |
| `.gitattributes` | Line-ending policy. The scripts run on Linux; a CR byte becomes part of the last word on every line and silently breaks pattern matching, `case` labels and the generated site list. |
| `LICENSE` | PolyForm Noncommercial 1.0.0. |

## Data flow

The manager grew a fleet layer on top of the per-site one in v6.2.0:

```
parse_args → validate_args → config_load → config_apply_all → validate_args
   │
   ├─ --list-modes / --print-config / --list-sites … inspect and exit
   │
   └─ log_init → lock_acquire → ensure_site_list → load_site_list
        → resolve_site_users → run_fleet
             ├─ run_sequential            (JOBS == 1)
             └─ run_batched               (JOBS > 1)
                  worker_init → worker_run & → wait → fold_worker
                       └─ process_site → maybe_backup → run_mode → mode_* → run_wp
        → print_summary (state file, JSON report, notification, human summary)
        → final_exit_code
```

`process_site` owns the policies that must not be duplicated per mode: the abort check
(`--max-failures`, `--run-timeout`), the `--skip-if-no-updates` question, the backup,
the retry loop, and the before/after version snapshots that feed the change log.
`run_mode` is the single dispatch table; adding a mode means adding one line there.

Four things a parallel worker cannot do on its own, and where each is handled:

| Problem | Where it is solved |
|---|---|
| a subshell cannot increment a parent counter | the worker zeroes its counters at fork and writes them to `${WORK_DIR}/wN/res`; `fold_worker` adds them up |
| concurrent sites interleave on the terminal | the worker's stdout is a fragment; the parent replays fragments in site order at the barrier |
| concurrent sites interleave in the shared log file | `log_write_file` appends to the fragment while `PARALLEL=true`; the parent appends fragments in order |
| a machine-readable payload must stay parseable | `render_plugin_table` and `emit_site_record` route to the fragment whenever the output is machine-readable |

```
Find_WP_Senior.sh --output FILE [ROOT ...]
        |
        |  one absolute path per line, atomic write, permissions preserved
        v
   wp-found.txt  <----------------------------- edited by hand, or --skip-existing
        |
        |  --sites FILE (or SITES_FILE, or the default next to the script)
        v
Bash_WP-CLI_Update.sh MODE [options]
        |
        |  per site: owner resolved from wp-config.php owner -> directory owner
        |            -> DB_USER, validated against the passwd database
        v
   runuser -u OWNER -- /bin/sh -c 'cd -- "$1" || exit 127; shift; exec "$@"' sh SITE \
        env DOCUMENT_URI=... DOCUMENT_ROOT=... HOMEDIR=... HTTP_HOST=... HOME=... \
        [timeout --signal=TERM NNN -k NN] wp --path=SITE [--allow-root] \
        [--skip-plugins=...] SUBCOMMAND ...
        |
        +-> stdout+stderr captured per call -> counters, console, manager.log
        +-> failures additionally -> errors.log with context, command and output
        +-> summary -> <log>.state, read back by --status
```

## Configuration layers

```
defaults (in the script)
   ^  overridden by
/etc/wp-cli-update.conf
   ^  overridden by
<script dir>/wp-cli-update.conf      (or the file named by --config)
   ^  overridden by
WP_CLI_UPDATE_<KEY> in the environment
   ^  overridden by
the command line
```

`--print-config` prints the result of that stack together with the winning layer
for each key. Values from a file or the environment are validated *before* the
merge (exit 4), values from the command line after it (exit 2), so the exit code
tells the operator which file to open.
