Rand Stats

App::Ariza

zef:apogee
Revision history for App::Ariza

0.2.4  2026-08-21T22:34:22+01:00
    - Require Template::Jinja2 0.3.0 (Python method dispatch, Jinja2-faithful
      filters, better template error messages).

0.2.3  2026-08-14T21:54:28+01:00
    - Fix Windows Notcurses startup failures with error 0x7e. Bundle
      launchers now put the exact staged Notcurses tag/lib directory first
      on PATH, followed by SQLCipher, across the .cmd, PowerShell and native
      runner sidecar implementations. NOTCURSES_NATIVE_DATA_DIR still points
      at the data root so Notcurses::Native retains its terminfo setup.
    - Give direct `{raku}` smoke commands the identical Windows PATH order,
      built on a minimal System32-only base instead of the CI host's MSYS2,
      Visual Studio and vcpkg toolchain PATH. This prevents a missing bundle
      dependency from being borrowed from the build machine.
    - Automatically exercise Notcurses in every declared bundle smoke without
      opening a terminal. The probe calls `nc-lib()` first, which makes the
      Windows resolver load the full FFmpeg-linked DLL closure with its sibling
      directory, then creates and destroys a one-pixel visual through the core
      library as an operational proof. A core-only version query can no longer
      hide a broken multimedia DLL closure.
    - Correct the Windows PE-audit contract: it proves that import-table
      closure is complete and adjacent inside the bundle, not that Win32's
      live loader will search beside an absolute top-level DLL. The launcher
      PATH and the target-platform full-library smoke are the runtime proof.
    - Scaffolded workflows now require
      `App::Ariza:ver<0.2.2+>:auth`, preventing a release lane
      from resolving an older builder that still emits the broken Windows
      launcher environment or lacks the full-library smoke.

0.2.1  2026-08-13T23:28:11+01:00
    - Correct the runner-v2 checksums shipped in 0.2.0. Windows bundle
      builds now accept the published x86_64 and ARM64 runners while still
      refusing any binary whose bytes do not match the embedded pin.
    - Verify the pinned runner release, its published checksum document and
      both actual binaries during Mi6's pre-upload distribution tests. A stale
      or incomplete runner pin can no longer reach Zef through `mi6 release`.

0.2.0  2026-08-13T22:49:37+01:00
    - Publish runner-v2
    - Added opt-in managed-install update prompts. Apps enable them with
      `[updates] enabled = true`; eligible interactive launches check the
      configured GitHub repository at most once a week and offer `Install &
      use`, `Ask next time`, or `Don't ask again for this version`. Portable
      bundles, redirected sessions, CI, help/version invocations and guarded
      relaunches bypass the check. Only the exact latest bare ASCII `X.Y.Z`
      release is eligible; prereleases, `v` tags and build suffixes are not.
    - Update-enabled bundles carry a generated, core-only Raku coordinator
      and a trusted snapshot of their platform installer. A chosen update
      downloads the exact candidate and mandatory SHA-256, validates the
      extracted manifest's application, repository, protocol and paths, then
      switches `current` transactionally. Public URL and insecure installer
      escape hatches are unavailable to this private path.
    - Successful updates retain exactly the new version and the physical
      version that was current immediately before the switch, with `previous`
      pointing at that rollback target. Windows records bounded deferred
      cleanup when the running bundle cannot yet be removed; later installs
      retry it without following reparse points or deleting a current,
      previous or running version.
    - POSIX launchers and the Windows native runner authenticate the installer
      handoff with a per-launch 256-bit nonce and a path-free protocol record
      before honoring reserved status 75. `Install & use` performs one guarded
      relaunch with the original argv; an application's ordinary exit 75 is
      returned unchanged. Update-enabled Windows builds require runner-v2 or
      later and refuse a script-only or runner-v1 bundle.
    - Generated release CI rejects non-bare tags for update-enabled apps, and
      bundle manifests, smoke checks and licensing inventories now account for
      the coordinator, installer snapshot and runner capability. Update
      support remains absent from bundles whose config does not opt in.
    - Generated workflows now require
      `App::Ariza:ver<0.1.4+>:auth`, so a builder that predates the
      `[updates]` table cannot silently ignore an app's opt-in.

0.1.3  2026-08-13T13:22:42+01:00
    - Generated CI installs App::Ariza with a 0.1.2+ version floor and
      the zef:apogee author pinned, with the full package spec shell-quoted.

0.1.2  2026-08-13T12:43:51+01:00
    - Windows PowerShell launchers no longer use PowerShell 7's ternary
      operator. The generated `.ps1` now parses under Windows PowerShell
      5.1 while retaining its LOCALAPPDATA-to-TEMP state-directory fallback.
    - `ariza installers` defaults its `--branch` to `HEAD`. The library
      moved to HEAD in 0.1.1 but `bin/ariza` kept declaring `main` as
      its own default, which is the value a user who runs the command
      actually gets — so the scripts rendered for the first release
      after that change still pointed at a branch the repository does
      not have, and the fix looked applied while the bug was fully
      intact. t/07-cli now renders through the CLI and fails on a
      hardcoded branch in any generated script, because a library
      default proves nothing about the command.

0.1.1  2026-08-13T01:14:07+01:00
    - The POSIX installer's parting message actually tells the user
      their current terminal cannot see the command yet, and gives
      them the paste-able `export PATH=…` line that fixes it there and
      then. The old message keyed on $PATH after ariza_persist_path
      had already exported the directory into the installer's own
      environment, so the tell-the-user branch was unreachable — the
      first real install ended with a command the terminal could not
      find and no explanation. The message now keys on what
      persist-path found before it touched anything.
    - Installer URLs and the generated release notes reference the
      `HEAD` ref instead of a hardcoded `main` branch.
      raw.githubusercontent resolves `HEAD` to the repository's default
      branch whatever it is called; `main` was a 404 for every
      `master`-defaulted repository — including the first app to ship
      with these installers, whose documented `curl | sh` line failed
      on its first real use. `:$branch` still overrides for anyone who
      wants a pinned ref.
    - The Windows installer smoke keeps Windows' mandatory ambient
      environment (SystemRoot, windir, ComSpec, PATHEXT, SystemDrive)
      when it builds its cleared-environment child. `env -i` has no
      Windows analogue: launching any .cmd resolves the interpreter
      through ComSpec, so the fully-stripped environment made the OS
      itself malfunction — ERROR_MOD_NOT_FOUND before the launcher ran
      a line — and failed the first published Windows installer smoke
      against a perfectly good bundle. These variables belong to
      Windows, not to the runner's toolchain, so keeping them does not
      weaken the isolation.
    - The Windows installer smoke also launches the installed
      `.exe` when the bundle ships one — the entry point PATHEXT
      actually hands a user who types the bare command was otherwise
      never exercised post-install, and it fails independently of the
      .cmd path (no cmd.exe, no ComSpec involved).
    - The scaffolded release workflow triggers on bare version tags
      (`[0-9]*.[0-9]*.[0-9]*`) as well as `v*`. mi6 — the release tool
      this scaffold's own audience uses — tags `0.3.0`, not `v0.3.0`,
      and the first real mi6 release of a scaffolded app pushed a tag
      no workflow fired on. The publish job was already prefix-agnostic
      (it gates on `refs/tags/` and names the release from the ref),
      so only the trigger list needed to learn the second shape.
    - A bundle's Raku code is installed into the bundled runtime's own
      `vendor` repository — `/rakudo/share/perl6/vendor` — and
      no longer into `/site`, which quietly cost every user a
      full recompile of the closure on their first launch. Rakudo
      records what a precompiled unit depends on relative to the
      repository holding it (`vendor#sources/`) for the four
      repositories its registry has a *name* for — `core`, `vendor` and
      `site` under the running interpreter's own prefix, `home` under
      `$HOME` — and as an absolute path on the compiling machine for
      every other one, including any directory named in `RAKULIB`. So a
      bundle built in CI and unpacked under `~/.local/share` looked for
      its own modules in the build agent's workspace, found nothing,
      declared all 52 compiled units outdated and rebuilt the lot: 58
      seconds on the first launch of the first bundle anyone installed,
      against 1.8 seconds now, for a store that shipped complete and
      correct. Nothing on the build machine could see it, because there
      the paths still resolve.
    - `ariza bundle` asks the bundled runtime what it calls that
      repository before it warms a single module, and refuses to build
      when the answer is nothing. It then reads the store's dependency
      records back and fails on any that name a path instead of a
      repository. Both checks exist because this failure is otherwise
      invisible at build time and expensive exactly once per user: the
      store is present, the right size, and worthless.
    - `ariza smoke` gained a `precomp-relocatable` check that reads the
      same records out of the unpacked archive. It is the one check in
      that command which cannot be replaced by running the bundle:
      smoke runs on the machine that built it, where a store bound to
      the build tree still works perfectly.
    - The manifest records `launcher.site`, the repository the launcher
      points `RAKULIB` at, so anything inspecting a bundle afterwards
      reads the answer out of the bundle rather than out of whichever
      ariza happens to be installed. `ariza smoke` uses it, and falls
      back to `site/` so that archives published before the move are
      still checkable on their own terms.
    - New `xt/05-relocation.rakutest`: builds a bundle of a two-module
      fixture app, unpacks it somewhere else, deletes the build tree,
      and fails if the relocated launcher recompiles anything —
      RAKUDO_MODULE_DEBUG for Rakudo's own account of it, and the
      store's file list for a second opinion. Deleting the build tree
      is the step the whole suite was missing.
    - The generated installers warm the app up before they say
      goodbye: they run the launcher they just linked, once, with the
      output suppressed and a line on screen saying so. Whatever a
      first launch has to do that later ones do not — paging a few
      hundred megabytes off a cold disk, building a per-user state
      directory — now happens while an installer is visibly working
      rather than the first time somebody wants the program. Every
      path that reaches the parting message goes through it, including
      the re-run that found the version already installed, which is
      what people try when the last one did not take.
    - `installer.warm` in ariza.toml says what to run: `--version` by
      default (the one invocation every bundled app answers and
      returns from, and already `bundle.smoke`'s canary), a string
      split on whitespace, an array taken word for word, or `false` to
      skip the step. An empty value is a load-time error rather than
      either of the things it might mean: a launcher run with no
      arguments starts the application, which does not return, and the
      installers impose no timeout on purpose. Arguments are rendered
      shell-quoted (and PowerShell-quoted, and splatted rather than
      passed as one array-shaped argument), because this is the one
      place where somebody else's string becomes script syntax.
    - A warm-up that fails warns loudly and the install still
      succeeds — no non-zero exit, no rollback. It deliberately
      diverges from the fail-closed posture of `ariza bundle` and
      `ariza smoke`, and the reason is the failure domain: those run
      in our own pipeline before anything is published, where stopping
      is cheap and correct, while this runs on a stranger's machine
      after the bundle has been downloaded, checksummed and put in
      place, where the same signal much more often means the machine
      (no terminal, a sandbox, a scanner) than the release. Failing
      there would delete a working program from somebody who has one.
      The message says what did not complete, and that the app is
      installed and worth running. The ruling and its reasoning are in
      design.md §6.6.

0.1.0  2026-08-12T22:34:02+01:00
    - The bootstrap-rung test captures the "no pinned runner build
      yet" notice instead of letting it reach the harness's stderr. A
      passing suite printing that line reads as a statement about the
      checkout's real pin file, which the test's fixture never
      consults — it confused exactly the person it should not have,
      twice. The capture also turns the notice into an assertion, so
      the loud-rather-than-silent contract is now tested rather than
      incidental.

    - Changed: the test suite's fixture application is now a fictional
      one -- `App::ExampleApp` / `exampleapp` / "Example App" 9.9.9,
      publishing from `example-org/App-ExampleApp` -- everywhere it used
      to be a real consumer of this tool. ariza is general-purpose, and a
      suite whose every golden launcher, installer, workflow and
      licensing document is branded for one application reads like that
      application is part of the product. It also meant the committed
      goldens named a repository that does not exist under an
      organisation that does. Illustrative examples in the Pod and in
      `docs/` are untouched: those describe using ariza against a real
      application, which is what they are for, and they are not fixtures.
      Every golden was REGENERATED through its own generator rather than
      edited, so what changed is the identity in them and nothing else;
      line endings are as they were, CRLF for the Windows launchers and
      the PowerShell installers.
    - Changed: `t/18-licensing`'s golden takes `ARIZA_REGENERATE_GOLDEN`
      like the launcher, installer and workflow goldens already did. It
      was the only one that could only be updated by hand, which is the
      same as saying it was the only one that could be updated from
      something other than the generator.
    - Note: the fixture's display name now contains a space ("Example
      App"). That is free coverage rather than an accident -- every
      template quotes `app_display` where it lands in code, and a
      fixture that never exercises a space is one that cannot notice if
      one stops being quoted.
    - Fixed: `ariza smoke` crashed on Windows whenever the bundle it was
      given was not a Windows one. The launcher check asked
      `IO::Path.x`, and the platform it branches on comes from the
      manifest rather than from the machine -- deliberately, so an
      archive can be checked anywhere -- so a Windows box inspecting a
      Linux bundle reached that call and died with `rindex search target
      requires a concrete string, but got null`. That message is
      MoarVM's, and the bug behind it is upstream: its `file-stat`
      syscall never populates the stat object's `filename`, and its
      Windows `stat-is-executable` reads exactly that field to find the
      extension to compare against PATHEXT, so `.x` on Windows does not
      return False -- it throws, for every file. Nothing here can fix
      that, but nothing here should have been asking: the executable bit
      needs both a POSIX bundle to have one and a POSIX host to read
      one, and a POSIX bundle unpacked on Windows has no such bit
      anyway, because NTFS has none and tar cannot set one. The check
      now asks only where there is an answer, and reports the file it
      found either way. The case that exposed it was the new
      cross-platform smoke fixture, which is the first thing in the
      suite to smoke a bundle whose platform disagrees with the host --
      and is now the regression test for it.
    - Fixed: `App::Ariza::Runner.pins` leaked a file handle on the one
      path it is designed to take -- dying part way through a malformed
      pin file. It read with `IO::Path.lines`, whose handle is closed
      when the sequence is exhausted, and a `die` on line two of ten
      exhausts nothing. POSIX never noticed; on Windows the file then
      could not be deleted at all (`resource busy or locked`), so the
      test that checks the parser's refusal blew up in its own cleanup
      several assertions later, naming nothing useful. It slurps and
      splits now -- the whole file read and the handle closed before a
      single line is examined, which is what every other file read in
      ariza already does -- and the test deletes the file it just fed to
      the parser, so the leak fails as an assertion about handles rather
      than as an explosion in a LEAVE block.
    - Added: a Windows bundle now launches from `bin/.exe`, a
      compiled launcher of ariza's own -- ~400 lines of C17 in the new
      `runner/` directory -- and it is the documented entry point. The
      `.cmd` and `.ps1` launchers still ship, still work, and are still
      what the bundle falls back to; what changed is which one users
      run, which one the installers put on PATH, and which one the smoke
      harness proves. Two things a batch trampoline cannot fix are why
      it exists. The first is that `%*` is not the user's arguments, it
      is the user's arguments after cmd.exe has had a second go at them:
      `^` is an escape character, `%VAR%` and `!x!` are expansions, and
      a quoted argument gets re-split, so an app taking a prompt, a
      regex or a snippet of code as an argument receives something
      nobody typed -- and the damage happens inside the substitution,
      where nothing can intercept it. The second is that AppLocker and
      SRP script rules block a `.cmd` however it is invoked, including
      from a parent process, and a locked-down ExecutionPolicy stops the
      PowerShell twin on its own, which made a bundle unusable at
      exactly the sort of managed desktop these apps are wanted on.
      Exec-ing the `.cmd` from an exe would have kept both problems, so
      the runner does the whole launch itself: GetModuleFileNameW for
      its own path, the directory above `bin/` as the root,
      SetEnvironmentVariableW for the environment, CreateProcessW for
      the child, and the child's exit code as its own. No cmd.exe is
      involved at any point.
    - Added: the argument tail is copied, never parsed. The runner
      follows the C runtime's argv[0] rule exactly -- leading whitespace
      skipped, a quoted argv[0] ending at the next quote with no escape
      processing (a path cannot contain one), an unquoted one ending at
      the first space or tab -- and hands the child every byte after
      that point unchanged. There is no re-quoting step for anything to
      be lost in, so what the app's `@*ARGS` sees is what the user's
      shell produced. The test table for that rule is the list of things
      a batch file damages: carets, `%VAR%`, `!x!`, escaped quotes
      inside quoted arguments, trailing backslashes before a closing
      quote, unicode, and an unterminated quoted argv[0].
    - Added: `bin/.ariza`, the sidecar the runner reads, rendered
      like every other launcher and CRLF like its neighbours. It carries
      the target script, the exec and display names, and then the
      bundle's environment as ORDERED DIRECTIVES -- `set NAME=VALUE`,
      `unset NAME`, `prepend-path VALUE`, applied top to bottom, with
      `{root}` expanded to the bundle root the executable worked out at
      run time. The runner has no idea what any of them mean. That is
      the point, and it is the second design ruling here: an earlier
      draft had `sqlcipher_dir` / `sqlcipher_lib` keys and a hardcoded
      NOTCURSES_NATIVE_DATA_DIR, which put knowledge of every native
      dependency a bundle might ever carry inside a pinned binary on its
      own release cadence -- so adding a dependency would have meant a
      runner release, a checksum commit, and a window in which a new
      bundle could not be launched by an old runner. With directives
      that knowledge stays in the renderer, beside the `.cmd` template
      where it already was, and a future app-declared "recipe" can
      contribute env lines without a sidecar format break.
    - Added: the parser fails closed, twice over. An unrecognised
      directive and an unknown `{token}` are both hard errors naming the
      line number, because a bundle whose configuration mentions
      something the runner cannot do would otherwise start an app with a
      silently incomplete environment -- which fails later, further
      away, and much worse than not starting. `#` starts a comment only
      in the first non-blank column, since `inst#{root}\site` is a real
      value and an inline-comment rule would truncate it into a RAKULIB
      naming a relative directory.
    - Added: `App::Ariza::Runner`, and the bootstrap ladder it
      implements. The runner is built once per release by CI and pinned
      by digest rather than compiled during `ariza bundle`, because
      compiling it at bundle time would put a C toolchain in the
      bundling path, on every machine, for every cross-build, to produce
      a file that is identical for every app. `resources/RUNNER_VERSION`
      names the release and `resources/runner-checksums.txt` pins the
      artefacts, and that pin file has exactly two states: while it
      lists nothing, a Windows bundle is built WITHOUT the executable
      and the build says so once, loudly, which is byte-for-byte the
      output ariza produced before the runner existed; once it lists
      anything, a missing entry, a failed download or a digest mismatch
      fails the build. That is the same precedent as Notcurses-Native's
      checksums.txt, and it is what lets this ship before the first
      `runner-v1` artefacts are cut without a placeholder anywhere.
      There is deliberately no third rung, no `--no-runner` and no
      `--skip-verify`: an unverified executable staged into a bundle is
      not a degraded build, it is a different piece of software.
    - Added: `.github/workflows/runner-release.yml`. Both lanes in MSYS2
      -- UCRT64 for x86_64, CLANGARM64 for aarch64, the same toolchains
      that build the notcurses packs a bundle already carries -- ctest
      on the x86_64 lane, and publication ONLY on a pushed `runner-v*`
      tag validated against `resources/RUNNER_VERSION`. A dispatch
      builds and tests and stops, because "validate the recipe" and
      "overwrite the artefacts every ariza release verifies against"
      should not be one click apart. The executable is linked statically
      and the lane then asserts its import table holds nothing but
      Windows' own DLLs: a `libgcc_s_seh-1.dll` picked up from the build
      environment would work on the runner, work on every machine with a
      toolchain, and fail on a clean install -- the identical failure
      shape to 0.0.7's vcruntime140.dll.
    - Added: the runner is split so the interesting half is portable.
      Config parsing, `{root}` expansion, path joining and the
      command-line-tail rule are plain C17 over a character type that is
      `wchar_t` on Windows and `char` everywhere else; the win32 API
      calls live in one file that is not compiled at all off Windows. So
      `cmake -B build -S runner && ctest --test-dir build` runs on a Mac
      or a Linux CI box and covers the rules that are actually easy to
      get wrong. One of those tests parses
      `t/golden/launcher-windows-x86_64.ariza` -- the committed output
      of the Jinja2 template, compared byte for byte by the Raku suite --
      with the real C parser, because the renderer and the reader are in
      different languages and nothing else connects them.
    - Added: `ariza smoke` runs every `{exec}` command a second time
      through `bin/.exe` when a bundle carries one, in addition to
      the `.cmd` run and never instead of it, plus a `runner` check for
      the executable and its sidecar. The two entry points set the same
      environment by completely different means -- batch `set` against
      SetEnvironmentVariableW, `%*` against a verbatim command-line tail
      -- so one passing says nothing whatever about the other. The
      check passes when there is no executable, because that is the
      bootstrap state and not a fault; it fails when there is one with
      no sidecar beside it, because that is a bundle that cannot start.
    - Changed: the generated Windows installer accepts either entry
      point. `Ariza-HasEntryPoint` looks for `bin\.exe` and then
      `bin\.cmd`, so the "incomplete install" and "that is not a
      bundle" checks stay true either side of the bootstrap. Nothing had
      to change about PATH: the installer puts `current\bin` on it, and
      PATHEXT already prefers an `.exe` to a `.cmd` of the same name.
    - Note: the published runner binaries are not code-signed, so
      SmartScreen shows its "unrecognised app" prompt on first run.
      Signing needs a certificate and a signing story ariza does not
      have today; saying so in the README, in the release notes and in
      the design record is better than a prompt nobody warned about, and
      the `.cmd` launcher is the way past one somebody would rather not
      click through.
    - Added: a bundle now says what it redistributes. Every build writes
      `THIRD-PARTY.md` at the bundle root -- one row per component with
      its version, its licence, its copyright and where that fact came
      from -- and a `LICENSES/` directory holding the text of every
      licence those rows cite. What it replaces was a table of filename
      prefixes inside `App::Ariza::Bundle` mapping `libav*` to FFmpeg
      and so on, which was wrong twice over: it had gone stale (it still
      declared the notcurses pack's FFmpeg a GPL build because of
      libx264, which that pack no longer ships) and it was knowledge
      about other people's software living in the source of a tool that
      bundles anybody's application. A stranger's app with a native
      dependency ariza had never heard of got a row saying
      `(unclassified)` and no way to fix it short of patching ariza.
    - Added: the new `App::Ariza::Licensing` reads all of it rather than
      remembering any of it, from four sources, and every row records
      which one it came from. A native pack's own licensing kit --
      `third-party.json` where it ships one, filtered to the platform
      being built, and its generated `THIRD-PARTY.md` where it does not.
      `resources/runtime-third-party.json`, ariza's maintained record of
      the components that have nowhere else to speak from. The `license`
      field of every distribution installed into the bundle, from its
      own site repository AND from the one inside the vendored runtime,
      which is where the zef that came down with Rakudo lives. And the
      app's `ariza.toml`, for what it ships that ariza cannot see. The
      row schema is deliberately the one Notcurses-Native's
      `third-party.json` already uses, because that format is an
      interface rather than something to reimplement, and the 0.2.0
      recipe work contributes rows in the same shape without a break.
    - Added: `resources/runtime-third-party.json` -- the vendored
      Rakudo, NQP and MoarVM (Artistic-2.0), and the C libraries MoarVM
      vendors under its own `3rdparty/`, which are the reason the file
      exists: they are compiled into `libmoar` rather than shipped as
      files, so no audit of a bundle's contents can find them and
      nothing in the bundle can be asked about them. libuv (MIT, plus
      BSD-2-Clause for `tree.h` and ISC for the inet_pton pair), dyncall
      (ISC), DynASM (MIT), LibTomMath (public domain), cmp (MIT),
      libatomic_ops (MIT -- its GPL files are build and test tooling
      that MoarVM's own 3rdparty/README states are in no built binary),
      mimalloc, rapidhash and zmij (MIT), musl's memmem (MIT, in a
      directory called `freebsd`), Steve Reid's SHA-1 (public domain)
      and msinttypes (BSD-3-Clause). Each was checked against the
      LICENSE file of the submodule at the commit MoarVM pins, not
      recalled. That set moves between MoarVM releases and which members
      are compiled in depends on the platform, so the file documents
      itself as a superset for the pinned runtime and names the
      `[rakudo]` pin as the trigger to re-check it: naming a library
      that did not make the binary costs a paragraph, omitting one that
      did is a notice not given.
    - Added: licence texts ship in `resources/licenses/` for the
      identifiers ariza can be expected to resolve -- Artistic-2.0, MIT,
      Apache-2.0, BSD-2-Clause, BSD-3-Clause, LGPL-2.1, LGPL-3.0,
      GPL-2.0, GPL-3.0, Zlib, ISC, Unlicense and a public-domain
      statement -- and are cited by SPDX identifier, with `-or-later`,
      `-only` and `+` resolving to the text they qualify and `AND`, `OR`
      and `WITH` treated as separators so every arm of an expression
      travels with the binary. Anything else comes from a pack's own
      `LICENSES/` or from a `license-files` entry in the app's config.
    - Added: `[licensing]` in `ariza.toml`. `strict` promotes an
      unattributed native pack from a warning to a failed build;
      `[licensing.app]` overrides any field of the application's own
      row (which otherwise derives from its META6 and its LICENSE file);
      `[[licensing.third-party]]` declares what ariza cannot see -- a
      font, a dataset, artwork -- and `[[licensing.dists]]` corrects a
      dependency whose own metadata is absent or wrong. Unknown keys
      warn and wrong types die, as everywhere else. `license-files`
      entries are paths inside the app's repository and an absolute one
      is refused: a licence text belongs to the repository that declares
      it, and an absolute path in a committed config file is a path that
      exists on one machine.
    - Added: the fail-closed rulings, and the asymmetry in them. A Raku
      distribution with no `license` field, or one whose licence has no
      shipped text, FAILS the build -- naming every such distribution in
      the closure at once rather than the first, since three unlicensed
      dependencies should cost one build and not three -- with both
      fixes: the field itself, or a `[[licensing.dists]]` row. The field
      exists, most of the ecosystem fills it in, and an "unknown" row
      there would be a hole nobody ever closes. A cited
      licence text that is nowhere fails the build for the same reason.
      A native pack with no licensing kit is a VISIBLE row saying so
      plus a warning, because there ariza has nothing true to say: the
      pack is somebody else's artefact, refusing to build would punish
      an app for its dependency's packaging, and dropping it would hide
      a redistributed binary. Whether that is fatal is the app's call,
      which is what `licensing.strict` is.
    - Added: a fallback for the pack every app actually has. The
      upstream generator writes `THIRD-PARTY.md` into the archive and
      keeps `third-party.json` in its own repository, so no pack in
      circulation carries the manifest. Rather than report a complete
      notice sitting inside the bundle as "unattributed", the document
      is parsed -- but only on its own terms: the Summary table's exact
      generated header is required, four columns and two labelled
      Details bullets are read, and anything else returns NOTHING and
      falls through to the unattributed row. Rows assembled out of the
      wrong columns would be worse than no rows.
    - Added: `ariza-manifest.json` gains a `licensing` object -- the row
      count, how many were unattributed, and the sorted set of SPDX
      identifiers the bundle is conveyed under -- so a release gate
      downstream can test it without parsing prose. `unknown > 0` means
      something in the bundle is unnamed, and the identifier set is
      where a copyleft component that arrived inside a native pack
      becomes visible to a policy that cares about one.
    - Added: `components.runner` in the manifest, closing the last gap
      in it. `bin/.exe` was the only artefact a bundle downloads
      with no URL and no digest recorded anywhere, while the runtime
      archive and SQLCipher both had one -- and it is the file a Windows
      user actually runs. `App::Ariza::Runner.stage` now returns what it
      staged (path, artefact, tag, URL and the digest it was verified
      against) rather than only where it put it, and
      `App::Ariza::Launcher.write` returns `{ written, runner }` so that
      account reaches the manifest.
    - Added: the merged document is deterministic. Rows sort by kind,
      then folded name, then id -- never by the order a directory was
      read in -- and nothing in it is a timestamp or a path from the
      build machine, so two builds of the same inputs produce
      byte-identical output and a golden test means something. Licence
      texts are deduplicated by filename in a fixed priority -- the
      app's copy, then a native pack's, then ariza's own -- and two
      sources offering the same name with different bytes warn, naming
      both, rather than resolving by whichever directory was walked
      first. Nearest to the software wins because ariza's texts are SPDX
      TEMPLATES, carrying ` ` where a real notice carries a
      name: an OFL font's text has its Reserved Font Name filled in, and
      a pack's own copy is the notice shipped beside its binaries, so
      the generic text is the fallback for a licence nobody supplied and
      never a replacement for one somebody did. A text no row cites is
      removed from `LICENSES/` rather than left over from an earlier
      build to mislead.
    - Added: three more licence texts -- AGPL-3.0, X11 and OFL-1.1 --
      taken from SPDX's own license-list-data, bringing the shipped set
      to sixteen. All three came out of a scan of real installed
      distributions rather than a guess at what might be wanted, and the
      X11 text is byte-identical to the one the notcurses pack carries,
      so a bundle with that pack merges the two without a word. A test
      enumerates the whole set: adding or losing a text is a decision,
      not a side effect.
    - Added: `NOASSERTION` support, as a declaration and only as a
      declaration. An app may write `spdx-license = "NOASSERTION"` in a
      `[[licensing.dists]]` or `[[licensing.third-party]]` row after it
      has looked and found nothing -- SPDX's own spelling for exactly
      that -- and the row is rendered distinctly, called out at the top
      of THIRD-PARTY.md beside the unattributed, and carries a GENERATED
      sentence (not the app's wording) saying licensing was not asserted
      and pointing at the component's own repository. No licence text is
      looked up, because there is none. `ariza-manifest.json` counts
      them in `licensing.noassertion`, apart from `spdx-ids`, so a gate
      reading the identifier set never sees NOASSERTION sitting in it
      looking like a permissive licence.
    - Added: three rulings that keep NOASSERTION from becoming a place
      to put things. A distribution whose OWN metadata says NOASSERTION
      still fails the build -- that is not a declaration anybody made
      about this bundle, it is "nobody has looked" spelled differently,
      and the fix is for somebody to look and then say so in the app's
      config with the evidence in `notes`. An application may not say it
      about itself: NOASSERTION means a third party's licensing could
      not be determined, and there is nobody to look on behalf of the
      thing being built. And `licensing.strict` refuses a bundle that
      contains one, because strict is the statement that this bundle
      ships nothing it cannot name and "we could not find out" is not a
      name -- the same rule the unattributed-pack check enforces,
      applied to the other way a bundle can carry something nameless.
    - Fixed: nothing derives a name from a resource's `.basename` any
      more. `zef install` stages every META6 resource under a
      content-hashed filename, so an installed ariza's `MIT.txt` is
      `0E9B31....txt` and its data file is `A3FCDF....json` -- which
      made the licence-text pool key on digests (every build failing on
      a row citing `LICENSES/MIT.txt`) and every runtime row cite a
      digest as its provenance. Both work perfectly from a checkout,
      which is the only place the suite runs, so
      `xt/04-installed-resources` now installs ariza into a throwaway
      repository and asks the installed copy for both. A resource's name
      is its META6 key; the file is only where the bytes are.
    - Fixed: `App::Ariza::Config` validated arrays of tables lazily, so
      a `[[licensing.third-party]]` row missing its required
      `spdx-license` did not complain until something happened to
      consume the result -- which was outside the caller's `try` and a
      long way from the file that was wrong. The rows are built
      eagerly now.

0.0.7  2026-08-12T15:58:10+01:00

    - Fixed, and it is the same class of bug as 0.0.6's missing OpenSSL
      one level further down: the Windows bundle was built against
      vcpkg's SQLCipher, which is an MSVC build, which imports
      `vcruntime140.dll`. That DLL is not part of Windows. It arrives
      with Visual Studio, with the Visual C++ Redistributable installer,
      or dragged in by some other application that shipped it --
      `kernel32.dll` and `ucrtbase.dll` are in System32 on every Windows
      10 machine ever installed, and `vcruntime140.dll` is on none of
      them by default. So the bundle loaded on the machine that built
      it, loaded on every CI runner, loaded on most developer laptops,
      and refused to load on a clean install, which is the only machine
      a bundle exists for. Every machine likely to notice was a machine
      where it was invisible; the release lane in particular could not
      have caught it, because a GitHub runner has the redistributable.
      The Windows lane now installs MSYS2's
      `mingw-w64-ucrt-x86_64-sqlcipher` instead, which imports
      `ucrtbase.dll` -- the UCRT, which Windows 10 and later ship.
    - Added: the redistributable gate. `PE-REDIST-DLLS` and
      `pe-redist-dll` name the family -- `vcruntime*.dll`, `msvcp*.dll`,
      `concrt*.dll`, `vcomp*.dll` -- and the PE audit now reports any
      staged file that imports one the bundle does not carry. They stay
      on `PE-SYSTEM-DLLS`, so nothing copies one in: they are
      Microsoft's to redistribute, on Microsoft's terms, and app-local
      deployment of them is a decision that belongs to whoever signs the
      release rather than to ariza. The finding carries the whole
      argument -- the clean-machine consequence, the UCRT-built library
      that makes the question go away, and the fact that carrying the
      runtime yourself satisfies the check too -- because the person
      reading it has just watched the bundle work. `ucrtbase.dll` is
      deliberately not in the family, which is the entire reason a UCRT
      build is the answer and not another instance of the question.
    - Added: Windows sourcing accepts either package manager's name for
      the library. vcpkg's port produces `sqlcipher.dll` and MSYS2's
      produces `libsqlcipher-0.dll`, so the canonical name wins where it
      is found -- per directory, in priority order, matched
      case-insensitively as the loader matches it -- and where no
      directory holds it the search widens to `libsqlcipher*.dll`,
      newest first by the same numeric-aware comparison the Linux pass
      uses for EPEL's renamed soname. Whatever is found is staged under
      the canonical name, which is safe for exactly the reason the Linux
      rename is: `LoadLibrary` resolves a DLL by the leaf name on disk
      and never consults the module's internal name, just as `dlopen`
      never consults `DT_SONAME`. `origin` records what was actually
      found, so the manifest never quietly renames anything.
    - Added: MSYS2 to the Windows search. `MSYSTEM_PREFIX/bin` first
      (whichever environment the shell is in), then a `VCPKG_ROOT` tree,
      then `C:\msys64\ucrt64\bin` and `C:\msys64\mingw64\bin`. vcpkg is
      still probed rather than dropped -- a machine that has only that
      should build rather than stop -- and the audit above is what
      refuses to ship the result.
    - Changed: the Windows CI lane lost three steps and a cache. The
      vcpkg port was a fifteen-minute source build, which is why the
      lane carried an actions/cache restore/save pair wrapped around it;
      the MSYS2 package is prebuilt, so both are gone and the install is
      one `pacman` call against the MSYS2 that every windows runner
      already has at `C:\msys64`. `-Sy` refreshes the package databases
      first, deliberately: the runner image's copy is as old as the
      image and MSYS2's mirrors keep only current versions, so a stale
      database sends pacman after a file that is no longer there.
    - Docs: the README is now about using ariza, and the reasoning that
      produced it moved to docs/design.md. One document was doing both
      jobs -- a section per verb interleaved with the ELF and PE
      walk-and-copy arguments, the vcruntime analysis and the no-mirror
      ruling -- so someone looking up `--sqlcipher-archive` read three
      pages about DT_SONAME on the way. docs/Readme.rakudoc is
      usage-first now: synopsis, what a build machine needs, a quick
      start, one section per verb with its options and a worked example,
      then reference material -- the bundle layout with what each
      directory is for, the complete annotated ariza.toml schema, the
      pin file, platform slugs, where SQLCipher comes from, what the
      generated installers and workflows do, and an environment index
      covering the five variables ariza reads while building, the five
      its launcher exports to the app, the two the installers honour and
      the three the test suite takes. Inline rationale survives only
      where it changes what a user does -- smoke runs with a replaced
      environment, so a bundle cannot pass by borrowing your shell's.
      Nothing was deleted: every relocated paragraph is in
      docs/design.md, organised as a design record and date-stamped
      against the release that produced each ruling, from 0.0.1's
      one-distribution-mechanism decision through 0.0.6's PE dependency
      walk to this release's redistributable gate.
    - Fixed: App::Ariza::CI's Pod still described the Windows build lane
      as `vcpkg install sqlcipher:x64-windows`, which this release's
      lane template replaced with MSYS2's UCRT package for the reason
      the same release's audit now enforces. Corrected in both the lane
      description and SEE ALSO, since a Pod that names the wrong package
      manager is how the next person reintroduces an MSVC build.

0.0.6  2026-08-12T11:45:28+01:00

    - Fixed, and this is the one that was actually breaking Windows:
      `App::Ariza::Native.stage-sqlcipher` had a macOS branch and a Linux
      branch for making the staged library self-contained, and no Windows
      branch at all. A Windows bundle therefore got `sqlcipher.dll` on
      its own, without the vcpkg OpenSSL it imports
      (`libcrypto-3-x64.dll`), so `LoadLibrary` failed while resolving
      imports on every machine that ran it -- surfacing as
      "DBDish::SQLCipher needs 'sqlcipher.dll', not found", a message
      about the DLL that *is* there produced by the DLL that is not. The
      two previous fixes for that symptom (0.0.5's `runtime-env` PATH,
      0.0.4's `zef.raku`) were both real bugs and neither could have made
      it go away, because there was no OpenSSL in the bundle for any
      PATH to point at. `!stage-pe-deps` now does for PE what
      `!stage-elf-deps` does for ELF: read the imports, skip the ones
      Windows provides, copy the rest in beside the library out of the
      source library's own directory, and recurse. A vcpkg port installs
      its whole runtime closure into one `installed//bin`, so
      that directory is the search space and the contract -- an import
      that is not in it fails the build naming the import, the directory,
      and the one other thing it could have been.
    - Added: `pe-imports`, a PE import-table reader in pure Raku (~80
      lines) -- DOS header to `PE\0\0`, COFF header, optional header in
      both PE32 and PE32+ shapes, data directory 1, the import
      descriptors, and the section table that maps their RVAs back to
      file offsets. Written out rather than shelled out to because there
      is nothing to shell out *to*: `dumpbin` ships with Visual Studio
      and `objdump` with neither, and a dependency walk that quietly
      finds nothing when its helper is absent is precisely how the hole
      above stayed open. Anything that does not parse dies naming the
      file and what was wrong with it; there is no "I don't know" return,
      because both callers would pass on one. Delay-load imports (data
      directory 13) are deliberately not read, and the Pod says why.
    - Fixed: the Windows half of the audit was a presence check --
      "the payload is there and is not empty" -- on the reasoning that a
      PE records no rpath to inspect. What it records is its import
      table, which is the entire question, and a bundle holding only
      `sqlcipher.dll` passed that check with two files and no findings.
      The PE audit now resolves every non-system import of every staged
      `.dll` against the directory the file sits in (which is how Windows
      resolves it, and where ariza stages everything) and requires the
      result to be inside the bundle, sharing the ELF audit's
      separator-normalised containment check. `not found` is a finding, so
      is a resolution outside the bundle, so is a `.dll` that is not a PE
      or is empty, and a PE the parser cannot read stops the audit rather
      than counting as checked. None of it needs Windows: the file is
      read, not run, so the Windows verdict is as strong from a Mac as it
      is on the machine the bundle is for.
    - `PE-SYSTEM-DLLS`, the Windows skiplist, is measured rather than
      guessed: the core Win32/UCRT/VC++/api-set names plus exactly the
      DLLs that the 119 DLLs of the published notcurses Windows pack
      import and do not carry (`gdi32`, `dwrite`, `secur32`, `winmm`,
      `bcryptprimitives` and a dozen more). Without them the new audit
      would have failed the Windows release lane on notcurses rather than
      on SQLCipher. `xt/03-pe-imports.rakutest` (network-gated, like
      xt/01) downloads that pack and re-measures the list against it,
      parses all 119 DLLs, and runs the real audit over them staged the
      way a bundle stages them.
    - t/11-native.rakutest builds real PE files byte by byte -- a
      hand-packed PE32/PE32+ with a live import table, which a downloaded
      DLL could never be malformed enough to replace -- and drives the
      parser, the skiplist, `pe-strays`, the staging walk and the audit
      off them: 28 subtests, up from 24.

0.0.5  2026-08-12T10:44:52+01:00

    - Fixed: `App::Ariza::Smoke.runtime-env` never gave a Windows
      `{raku}` smoke command SQLCipher's location, so it died with
      "DBDish::SQLCipher needs 'sqlcipher.dll', not found" even though
      the DLL was staged correctly and the launcher templates already
      set the same two variables. Windows resolves a DLL off `PATH`
      rather than a linker environment variable, so `runtime-env` now
      prepends the bundle's `native/sqlcipher` onto `PATH` and sets
      `DBIISH_SQLCIPHER_LIB` to the DLL's absolute path there, mirroring
      what `launcher-windows.ps1.j2` / `launcher-windows.cmd.j2` already
      export before exec'ing raku (those templates needed no change —
      verified against their goldens). t/14-smoke.rakutest gained
      Windows-slug assertions for `runtime-env` alongside the existing
      Linux ones.

0.0.4  2026-08-12T10:05:02+01:00
    - Fixed: bundling for Windows died before it got near zef --
      `App::Ariza::Rakudo.zef-bin` assumed a `zef.bat` in the runtime's
      site `bin/`, and the official Windows zip ships no such thing --
      only `zef.exe`, `zef.raku` and the `-m`/`-j`/`-js` variants.
      `zef-bin` now returns `zef.raku` on Windows: the `.raku` stub
      (`sub MAIN(*@, *%) { CompUnit::RepositoryRegistry.run-script("zef") }`)
      is byte-identical across every platform zef ships for, so running it
      under the bundled `raku` resolves zef purely out of that raku's own
      site repository, with no assumption about a wrapper's format.
      `zef.exe` would work too, but a compiled wrapper's own
      raku-discovery behaviour inside a relocated bundle is unverified.
      App::Ariza::Site gained `zef-cmd`, the pure function that decides
      how to invoke whichever `zef` it is handed -- exec the shell wrapper
      directly on POSIX (unchanged), or run the `.raku` stub under `raku`
      on Windows -- read off the file itself, not off a platform slug.
    - Fixed: the Linux release lane's SQLCipher build step died on its
      own last line. `ldconfig -p | grep -i sqlcipher` ran under
      `set -euo pipefail` after a build that had fully succeeded, and
      exited 1 -- `ldconfig` indexes by SONAME, which
      `--dll-basename=libsqlcipher --soname=legacy` deliberately sets to
      `libsqlite3.so.0`, so the grep can never match. This is the exact
      trap xxt/linux-selfcontain-proof.sh's `find-sqlcipher-lib` already
      worked around; the CI template's sanity check now asks the
      filesystem the same way (`test -f
      /usr/local/lib/libsqlcipher.so.0` + `ls -l`) instead of the linker
      cache.

0.0.3  2026-08-12T09:38:21+01:00
    - `scaffold-ci` now gives every declared platform with a GitHub-hosted
      runner its own clean-machine installer smoke, not just Linux:
      `smoke-installer-macos-arm64` (the only coverage install.sh's BSD
      branches -- `shasum -a 256`, bsdtar -- ever get), the renamed
      `smoke-installer-linux-x86_64-glibc`, and a new
      `smoke-installer-windows-x86_64` that downloads the published
      windows-x86_64 bundle, runs the committed install.ps1 against it
      with `-Url`, and launches the installed `.cmd` under a
      from-scratch environment built with `System.Diagnostics.Process`
      (PowerShell has no `env -i`). The macOS/Linux/Windows split lives in
      one place, `%LANES` in App::Ariza::CI, alongside each platform's
      build lane; a platform with a build lane but no smoke recipe yet
      renders no smoke job and a rendered comment says so, instead of
      silently having none.
    - Fixed: the Linux release lane's SQLCipher build, broken by the first
      real `release.yml` dispatch. `--enable-tempstore` is gone in 4.14 --
      SQLCipher moved to SQLite's autosetup configure, which spells the
      same flag `--with-tempstore` -- and 4.14 links OpenSSL 3 (its codec
      calls `EVP_MAC_*`, absent from EL8's default OpenSSL 1.1), which EL8
      parallel-ships under /usr/include/openssl3 and /usr/lib64/openssl3
      rather than as the system OpenSSL. The build now installs
      openssl3-devel, points CFLAGS/LDFLAGS at that tree, and passes
      `--dll-basename=libsqlcipher --soname=legacy` -- autosetup otherwise
      names the library libsqlite3, not the libsqlcipher.so.0 every
      consumer here looks for -- plus an /etc/ld.so.conf.d entry, since
      EL8's ldconfig does not search /usr/local/lib by default.
      xxt/linux-selfcontain-proof.sh carried the same dead flag and an
      OpenSSL 1.1 recipe; it now derives its source-build version from
      resources/versions.toml's `sqlcipher` pin instead of a stale
      hardcoded default, and shares the fixed recipe.
    - Fixed: the Windows release lane installed ariza (and everything else
      in the job) under `defaults: run: shell: bash`, which made zef --
      including the one `ariza bundle` runs internally for the bundled
      site install -- resolve `tar` off Git Bash's MSYS build. GNU tar
      reads a leading `C:` in a path as a remote hostname, so any
      mixed-separator temp path zef handed it aborted the extract. The
      lane now runs under the runner's default pwsh instead (proven
      working already by App-Moneymoor's test.yml Windows job), with the
      bundle and smoke steps ported to PowerShell and their exit codes
      checked explicitly against `$LASTEXITCODE`, the same pattern the
      installer-smoke workflow already uses -- PowerShell does not fail a
      step on a nonzero native exit code the way bash's `set -e` does.

0.0.2  2026-08-12T03:02:19+01:00
    - Every Windows CI failure across the suite's first two runs on a
      real Windows machine was a genuine portability bug rather than a
      test being fussy, and four of them were in lib/. All are fixed
      below; the fixes are fixture-tested from every platform.
    - `sha256-file` now falls through instead of stopping at the first
      tool that disappoints it. sha256sum, `shasum -a 256` and
      `certutil -hashfile` are each tried in turn, and absence, an
      unspawnable binary, a non-zero exit and output that holds no digest
      are all "try the next one"; only an exhausted list dies, naming
      what every attempt did. The Windows failure this fixes: a GitHub
      runner has a `shasum` on PATH that is a Perl script with no
      interpreter association, so `where` finds it and CreateProcess
      refuses it -- which took down every digest on the machine, and with
      it t/08, t/09 and t/11 mid-file, while certutil sat untried behind
      it. certutil stays last because Linux distributions ship an
      unrelated NSS tool of that name; being last means its refusal is
      simply one more failed attempt. There is still no "skip
      verification" path. `sha256-file` takes the usual `:&run` seam now,
      so the output parsers -- including certutil's banner, its
      upper-case hex and the spaced-out byte pairs older builds print --
      are covered by unit tests on machines that have never seen one.
    - `resource-list` returns forward-slash paths on every platform. In a
      checkout it walks the filesystem, and `.absolute` speaks the native
      separator, so Windows was handing back `templates\ci\...` for what
      is a META6 `resources` key -- the string that gets fed back through
      `resource` and compared against the manifest, and forward-slash by
      the packaging spec. Normalised at the one seam where the filesystem
      is consulted, so a checkout and an installed run agree.
    - New .gitattributes declaring `* -text`. The golden files in t/ are
      compared byte for byte, read with `slurp(:bin)` precisely so a
      line-ending difference cannot be shrugged off; Windows CI images
      set `core.autocrlf=true`, which rewrote every LF golden on checkout
      and failed t/12, t/15 and t/16 against output that was rendered
      correctly. `-text` turns conversion off in both directions, which
      also preserves the goldens that are deliberately CRLF -- the
      Windows launcher twins and the PowerShell installers, which exist
      because cmd.exe mis-parses an LF-only batch file. The templates
      those goldens are rendered from are covered by the same rule: a
      template rewritten on checkout would silently change what ariza
      ships. The three golden comparisons now also diag when the only
      difference is CRLF, so a stale working tree says so instead of
      printing two identical-looking scripts.
    - Test fixtures that spoke POSIX now speak whatever the platform
      does. t/01 and t/09 and t/12 normalise the separator before
      comparing a relative path against the bundle-internal spelling it
      is asserting about; t/14 derives its expected environment values
      from the same IO::Path it hands the code under test, since a bare
      `/b` acquires a drive letter on Windows and the assertion is that
      the environment names the bundle root rather than that it spells it
      the way macOS would; t/08 writes its digest fixtures as bytes,
      because a digest is a statement about exact bytes and a text write
      is entitled to opinions about line endings.
    - `sha256-file` no longer asks any tool to hash an empty file; the
      SHA-256 of empty input is a constant, and both certutil and
      sha256sum answer it badly. certutil cannot map a zero-byte file at
      all and fails outright (`ERROR_FILE_INVALID`, a documented quirk of
      the tool rather than a transient), and sha256sum, given a Windows
      path (backslash separators), switches to GNU coreutils' "escaped
      output" format, fusing a leading backslash onto the digest -- which
      a parser anchoring the hex at the start of the line then misses
      entirely, reading "exit 0, but no digest in its output". The digest
      parser is fixed too, independent of the short-circuit: it now takes
      the first 64-character run of lowercase hex anywhere on the line,
      so a leading backslash simply isn't part of the match.
    - The Linux ELF audit's containment check normalises both sides of
      the comparison to forward slashes before deciding whether a
      resolved dependency lands inside the bundle. `ldd`-style resolution
      hands back a target spelled in the host's native separators, while
      the `:@inside` bundle-root prefixes may not share that spelling --
      and on Windows CI a bundled `libcrypto.so.3` resolving to a path
      genuinely inside `native/sqlcipher` was flagged as a stray purely
      because one side had backslashes and the other did not.
      Containment is a path-identity question, not a byte question.

0.0.1  2026-08-12T02:13:21+01:00
    - The Windows release lane caches SQLCipher instead of building it
      from vcpkg on every run. `actions/cache/restore@v4` restores
      `C:\vcpkg\installed` under the key `vcpkg-sqlcipher-x64-windows-v1`
      before the build, and the build step itself is gated on the miss.
      The save is a separate `actions/cache/save@v4` step immediately
      after the build, not the combined `actions/cache` action, which
      only saves in a post step gated on the job succeeding -- a failed
      smoke later in the same run would otherwise discard a fifteen-
      minute build every single time. The dll-locating step that writes
      SQLCIPHER_LIB_DIR stays unconditional, since it has to run whether
      the library was just built or restored from cache.
    - The legacy installer machinery is retired. App::Ariza::Legacy, the
      eight Jinja2 templates and two shell partials under
      resources/legacy, the per-app configs, the SQLCipher mirror
      reference material, `ariza render-legacy` and the parity test are
      all gone. The ruling behind it: apps get retired rather than
      carried, and an app that is ready to ship ships the way Moneymoor
      does -- a bundle, four generated installer scripts and two
      workflows. Keeping a second distribution mechanism alive for one
      unreleased app meant two ways to install everything, two things to
      keep true of every pin bump, and a parity gate that could only ever
      say "the old thing still renders the old bytes".
    - The absorption was not wasted. The parts of the legacy partials
      that a bundle installer still needs -- the marker-block PATH
      persistence, the shell-rc detection -- were adapted into
      resources/templates/installer-common-{posix.sh,windows.ps1} when
      `ariza installers` was written, and those stay. What has been
      deleted is only what drove package managers, refcounted a shared
      registry and installed a terminal emulator: everything a bundle
      exists to not do.
    - versions.toml loses `wezterm`, `wezterm-macos-sha256` and
      `sqlcipher-release-base`. Each was read by exactly one consumer,
      the legacy render context, and by nothing else in lib/ or in any
      live template. `sqlcipher` stays (the advisory pin a bundle is
      expected to carry), `rakudo-star` stays, and `[rakudo]` -- the
      runtime a bundle actually embeds -- is untouched. The matching
      accessors and their tests go with the keys.
    - `rakudo-star` follows in the same cleanup: the legacy renderer was
      its only consumer, and with that gone the pin named a Rakudo
      release nothing in ariza installs on anyone's behalf. `sqlcipher`
      and `[rakudo]` are unaffected; App::Ariza::Versions drops the
      accessor and the parse arm, and their tests go with it.
    - Documentation stops naming the directory it was developed in. The
      distribution description, the NAME line and the example output
      paths no longer carry a private path segment, or imply that the
      apps live together in one tree: ariza is a bundler for Raku
      terminal applications, not for one person's directory of them. The `releases/latest` URL in the
      installer section now uses the C<< ... >> Pod form, since the
      E/E escapes rendered literally through C<>.
    - Phase four: `ariza scaffold-ci`. App::Ariza::CI writes the two
      GitHub Actions workflows an app needs into its own
      .github/workflows -- a test.yml in the house shape, and a
      release.yml that builds one bundle per platform the app declares,
      smokes each one, and publishes them on a tag. `ariza bundle`
      builds a bundle on the machine it is standing on; a release needs
      one per platform, on machines that are actually those platforms,
      because SQLCipher comes off the build host and there is no
      cross-build to fall back on. That is a CI problem, so this is CI.
    - One build lane per declared slug, each installing what ariza needs
      before running the same three steps (bundle, smoke, upload):
      macos-latest with `brew install sqlcipher`; ubuntu-latest inside
      quay.io/pypa/manylinux_2_28_x86_64, so the archive's glibc floor
      is 2.28 (RHEL 8+, Ubuntu 18.10+, Debian 10+) rather than whatever
      the runner image ships this month; windows-latest with
      `vcpkg install sqlcipher:x64-windows` and SQLCIPHER_LIB_DIR
      pointed at the result, which is the sourcing contract
      App::Ariza::Native documents for Windows.
    - The manylinux lane installs Rakudo by hand, out of the same JSON
      release index App::Ariza::Rakudo reads and at the same [rakudo]
      pin: Raku/setup-raku installs into the runner's tool cache, which
      is not in the container's filesystem, and there is no URL pattern
      to construct because upstream filenames carry a toolchain suffix.
      It builds SQLCipher from source at the pinned version too --
      EPEL's package is a 3.34.1-era build with a renamed soname, which
      ariza can now find but which is years behind the pin.
    - Every lane's Raku is there to RUN ariza and is not what ends up in
      the bundle; ariza downloads the pinned runtime for itself. Which
      is why setup-raku asks for `latest` and nothing in the workflow
      pretends otherwise.
    - publish is tag-gated, collects every lane's artefact, recomputes a
      combined checksums.txt and then checks it against the .sha256
      sidecars ariza wrote -- a release whose digest refuses its own
      archive is worse than one with no digest -- before cutting the
      release with a body that says what a bundle is, which machines
      each archive runs on, and how to verify a download.
    - smoke-installer is the job that matters: on a plain ubuntu-latest
      runner with nothing installed on it, it downloads the archive that
      was JUST published, installs it with the repository's own
      committed install.sh, and runs the installed launcher under
      `env -i`. It is the only check that exercises the artefact a user
      will actually receive by the path they will actually take, and it
      reads install.sh out of the checkout rather than from a raw
      GitHub URL so that it works for the first release.
    - workflow_dispatch triggers the build lanes and stops there, with
      an optional `ref` input. That is the iteration loop and the reason
      the trigger exists: a recipe that has gone stale costs a run and a
      push to a branch rather than a burnt tag and a deleted release.
    - release.yml is derived from bundle.platforms and is rewritten in
      place on every run. test.yml is written only when it is ABSENT --
      a test workflow acquires system dependencies, extra jobs and skip
      conditions no generator can infer from a manifest -- unless
      --force says otherwise, and each generated header says which of
      the two it is. The scaffolded test.yml names the app's
      bundle.native libraries and says it is deliberately not installing
      them: what a bundle carries and what a test run needs on a runner
      are different questions.
    - A declared platform with no lane (musl, aarch64 Linux, the Intel
      Mac, Windows on ARM) is a hard error rather than a skipped job,
      the same reasoning as an unknown slug in bundle.platforms:
      silently dropping one produces a release quietly missing a
      platform the author asked for. The three that do have lanes are
      the ones with both a GitHub-hosted runner and an official Rakudo
      build behind them, which is deliberately the same shape as
      App::Ariza::Rakudo's partial platform map.
    - ariza.toml gains an optional [ci] table with one key,
      `ariza-source`: "fez" (the default) or anything zef can install
      from, for an app whose release workflow has to exist before ariza
      is published. Whichever is not in use is rendered beside it as a
      comment, along with the ariza version that scaffolded the file, so
      switching is an uncomment rather than a remembering exercise. Not
      a closed set, so only an empty value is an error -- it would
      render a step that installs nothing and succeeds.
    - Golden-file tests cover both rendered workflows, with the pins and
      the ariza version supplied as fixtures so a golden fails when a
      template changes rather than when a version is bumped, and
      structural tests assert the things a golden diff would not
      explain: the job list matches the declared platforms, publish
      names every lane in `needs:`, both publishing jobs are tag-gated,
      the container line is there, and no unrendered Jinja2 delimiter
      survives (GitHub's own `${{ }}` does, which is exactly the
      collision the templates escape). Verified with actionlint,
      shellcheck and pyflakes: zero findings on both files.
    - Sourcing SQLCipher on Linux no longer requires the canonical
      libsqlcipher.so.0 soname. EPEL's `sqlcipher` package renames it
      per version -- libsqlcipher-3.34.1.so.0, and nothing literally
      called libsqlcipher.so.0 at all -- which failed sourcing on every
      RHEL-family host. The exact canonical name still wins outright
      when it is present; where it is not, any libsqlcipher*.so* from
      the same two sources (ldconfig -p, then the standard library
      directories) is considered, and the newest of several candidates
      is taken by a numeric-aware comparison rather than a plain string
      sort, with the manifest's origin naming what was actually found.
      What lands in the bundle is unaffected either way: stage-sqlcipher
      always copies to the canonical name, which is safe because
      `is native('sqlcipher')` dlopens by leaf name through
      LD_LIBRARY_PATH -- filename, never the file's own DT_SONAME.
    - The Fedora/RHEL install remedy in the "no SQLCipher on this
      machine" message now names the real package, `dnf install
      sqlcipher` -- `sqlcipher-libs` does not exist and was never
      installable advice.
    - A Linux bundle now carries its own OpenSSL. It did not, and the
      audit could not tell: a distro libsqlcipher.so.0 names OpenSSL as a
      bare `NEEDED libcrypto.so.3` -- no path, no RPATH, nothing in the
      file to object to -- which at run time means "whatever libcrypto
      this machine has", and on a user's machine may mean nothing at all.
      Staging an ELF now does what staging a Mach-O has always done:
      walks the library's dependencies with ldd (the loader itself, so
      the answer is the one the machine will really give), copies every
      non-system one in beside it under its soname, recurses, and runs
      `patchelf --set-rpath '$ORIGIN'` over the library and every copy.
      The skiplist -- what stays dynamic -- is Notcurses-Native's
      bundle-elf.sh list (the loader, libc, libm, libpthread, libdl,
      librt, libgcc_s, libstdc++ and friends), with one deliberate
      difference: libcrypto and libssl are NOT on it. That script leaves
      them dynamic because ffmpeg's use of them is optional. SQLCipher
      without OpenSSL is SQLite.
    - The ELF audit grew the two checks that can actually see this, and
      they only run on a Linux host because both tools answer for the
      machine they are on: `patchelf --print-rpath` must be
      $ORIGIN-relative (and present, on any file with a non-system
      NEEDED), and `ldd` in a REPLACED environment -- bare PATH, i.e.
      `env -i`, which is the user's view rather than the build's -- must
      resolve every non-system dependency to a path inside the bundle,
      with "not found" a finding. From a Mac, cross-inspecting an ELF is
      the static half and says so rather than pretending to more.
    - xxt/linux-selfcontain-proof.sh proves the above in a manylinux
      container rather than asserting it: real SQLCipher, real Rakudo,
      App::Ariza's whole test suite run on Linux, a real bundle staged
      and audited through App::Ariza::Native, the result re-checked
      independently in shell, and three negative controls (a planted
      absolute NEEDED, a deleted dependency, a stripped rpath) that must
      each make the audit fail. A proof that only ever passes proves
      nothing. It is xxt/, so nothing in t/ needs docker.
    - Binary formats are now read from the file's own magic number
      instead of shelling out to `file`. A format probe that answers "not
      a binary" when its helper is missing turns the audit into a loop
      that checks nothing and passes silently, and `file` is exactly the
      sort of tool a minimal build image leaves out.
    - SQLCipher now comes from the build machine's package manager, and
      there is no mirror to download it from -- the one named by
      `sqlcipher-release-base` never existed, and standing one up would
      have been a second piece of release infrastructure with its own
      signing story, staleness and outages for a library whose ABI moves
      about once a year. macOS takes the installed Homebrew keg (or, if
      it is not installed, the bottle `brew fetch` puts in the cache);
      Linux takes whatever `ldconfig -p` resolves libsqlcipher.so.0 to,
      falling back to the Debian multiarch, Red Hat and Alpine library
      directories; Windows takes a vcpkg tree under VCPKG_ROOT. CI
      installs the distro package before it calls `ariza bundle`.
    - What makes a package-manager library safe to ship is unchanged and
      untouched: the self-containment pass still copies every dependency
      that resolves outside the bundle in beside it, rewrites it to
      @loader_path, re-signs, and the audit still refuses to package a
      bundle where anything points off the build machine.
    - --sqlcipher-archive is unchanged and still beats everything;
      SQLCIPHER_LIB_DIR joins it as a second explicit override, naming a
      directory rather than an archive. Both bypass the new cross-build
      guard, which refuses to hand a `linux-x86_64-glibc` build the
      macOS keg it is standing on -- a library installed here is built
      for here, and an artefact made that way fails at dlopen on every
      machine it was named for. Every "not installed" path dies naming
      the package to install AND the override to pass.
    - The `sqlcipher` pin is now advisory. The package manager decides
      what is installed, so ariza reads the version out of the staged
      library's own bytes (SQLCipher keeps CIPHER_VERSION as a
      NUL-terminated X.Y.Z constant; filenames are no use -- a Homebrew
      keg's real file is libsqlcipher.3.51.3.dylib, which is SQLite's
      version, and Debian's is libsqlcipher.so.0.8.6, which is libtool's
      current.revision.age) and warns on a mismatch rather than failing.
      ariza-manifest.json records the version STAGED, the pin beside it
      as `pinned`, the digest of the library as it came off the machine,
      and which keg, bottle or archive it came from. It used to record
      the pin, which was a number nobody had verified.
    - Every slug ariza can name can now stage SQLCipher, musl and
      windows-arm64 included: with the library coming off the machine
      there is no per-slug publication question left to answer, only
      "is it installed".
    - Phase three: `ariza installers`. App::Ariza::Installer renders the
      four scripts an end user actually runs -- install.sh, install.ps1,
      uninstall.sh, uninstall.ps1 -- from the app's ariza.toml, into the
      app's own repository by default, because `curl … | sh` has to be
      able to fetch them from somewhere.
    - One POSIX script covers macOS and Linux, rather than the legacy
      pair. A bundle installer drives no package manager, so the only
      per-platform decision left is which asset to download, and that is
      a uname call away at run time: one curl line in a README, and
      nobody has to work out which of two files applies to them. The
      Linux branch probes for a ld-musl-*.so.1 loader before choosing
      between -glibc and -musl, exactly as App::Ariza::Platform does.
    - Detection produces one of the slugs the app *declares* in
      bundle.platforms or nothing at all -- never a nearest match, since
      a glibc bundle does not run on Alpine. An unnamed platform gets
      "no prebuilt  bundle for   yet" and a link
      to the releases page. An app that declares no Windows platform gets
      no install.ps1 at all.
    - Versions resolve from the `location:` header of the
      releases/latest redirect: one HEAD request, no API token and no jq
      on a machine that may have neither. --version names a tag instead.
    - Every download is verified against the .sha256 asset published
      beside it, and a missing or wrong digest is fatal. --url (or
      _BUNDLE_URL, e.g. MONEYMOOR_BUNDLE_URL) installs from a
      source the user names and bypasses GitHub entirely -- it takes a
      plain file path as readily as a URL, which is what makes an
      air-gapped install and ariza's own network-free end-to-end test
      possible. --insecure-no-verify applies to that path only; a
      published release always has a digest, so its absence there stays
      fatal.
    - The layout is $XDG_DATA_HOME//versions// with a
      `current` symlink and a ~/.local/bin/ link. The new version
      is unpacked into a staging directory on the same filesystem and
      renamed into place before anything is flipped, so a failed or
      interrupted download cannot damage a working install. One previous
      version is kept to roll back to and anything older is pruned, by
      name. Version directories are stamped at install time, not build
      time: tar restores the original mtime, which is identical for every
      bundle cut from one source tree.
    - Re-running for a version already installed downloads nothing. It
      re-points both links, re-checks the PATH block and exits 0 saying
      "already installed", which makes "run the installer again" the
      correct advice for the most common breakage.
    - PATH persistence is the legacy dp_persist_path machinery, adapted
      into an installer partial rather than sourced whole: a marker block
      named after the executable (so uninstalling one app never removes
      the line another needs), added only when ~/.local/bin is not
      already on PATH, and only once per rc file. The uninstaller removes
      exactly that block, rewriting through the existing file so its mode
      and ownership survive, and leaves the state directory and the app's
      own data alone -- naming them rather than deciding for you.
    - The generated scripts are curl-pipeable: entirely definitions with
      a single `main "$@"` on the last line, so a truncated download
      cannot half-run one, and nothing reads standard input, because when
      the script arrives *on* standard input there is nothing left to
      read from.
    - Windows mirrors all of it in PowerShell: %LOCALAPPDATA%\\
      versions\, a `current` junction (a symlink needs
      administrator rights or Developer Mode, which a per-user install
      has no business demanding), and ...\current\bin added once to the
      user PATH in HKCU\Environment -- which means an upgrade needs no
      PATH change at all. Archives are unpacked with tar.exe, which has
      shipped since Windows 10 1803 and reads the .tar.gz ariza actually
      publishes for every platform; Expand-Archive handles a .zip if one
      is ever pointed at with -Url.
    - ariza.toml gains an [installer] table with one key, `repo`, the
      GitHub owner/name releases come from. Optional in the schema and
      required by `ariza installers`, which says so rather than
      rendering a script that 404s. Its shape is closed like a platform
      slug's: a full URL or a bare name is a hard error.
    - Golden-file tests cover all four rendered scripts, `sh -n` checks
      the POSIX pair, and a quoting gate walks both with the shell's own
      quoting rules -- command substitutions and here-documents included
      -- to prove every path expansion is inside double quotes. A new
      xt/02-installer-e2e.rakutest drives a real bundle end to end
      against a scratch HOME: install, launcher runs and reports its own
      version, re-install repairs, upgrade flips current, a third install
      prunes to one previous, a missing or wrong digest is refused
      without disturbing the working install, and uninstall leaves both
      rc files byte-identical to what they were.
    - Phase two: `ariza bundle` and `ariza smoke`. A bundle is one
      directory and one archive containing the app, a Rakudo runtime,
      every Raku dependency with warm bytecode, and every native library
      it loads. Nothing is installed, nothing is written outside it (bar
      a one-line first-run marker under XDG_STATE_HOME), and deleting the
      directory is the uninstall.
    - App::Ariza::Rakudo resolves the [rakudo] pin against rakudo.org's
      JSON release index, downloads the archive once into
      $XDG_CACHE_HOME/ariza/rakudo and unpacks it into the bundle. The
      cache records a sha256 on first download and verifies it on every
      reuse; a mismatch discards the copy and re-fetches rather than
      baking a bad byte into every bundle built on that machine.
      rakudo.org publishes binaries for four of ariza's eight slugs, and
      asking for one of the other four dies naming the four that work.
    - App::Ariza::Site installs the app and its whole closure into the
      bundle's own repository using the *bundled* zef, so the bytecode
      matches the runtime that ships. It then warms the precompilation
      store, which zef does not do: measured on Moneymoor, a cold bundle
      spends ~55s compiling on first launch (and again on every launch,
      if the bundle is somewhere unwritable) against ~0.5s warm. The
      store is position-independent, so warming it at build time survives
      being unpacked anywhere, including a path with spaces.
    - Site checks what it built: the app distribution is present, every
      installed distribution's dependencies are satisfied from inside the
      bundle, every module the app provides loads, the precomp store is
      not empty, and notcurses staged a non-empty library directory under
      the BINARY_TAG it actually used (read off disk, never hardcoded).
    - App::Ariza::Native stages SQLCipher — into rakudo/lib on macOS,
      which is on the bundled interpreter's own LC_RPATH and so resolves
      `is native('sqlcipher')` with no launcher involvement at all; into
      native/sqlcipher elsewhere, where the launcher names it explicitly.
      On macOS the staged library is made self-contained: every absolute
      dependency (a Homebrew OpenSSL, say) is copied in beside it,
      rewritten to @loader_path, and everything touched is re-signed,
      because install_name_tool invalidates the signature and arm64 macOS
      will not load a library whose signature is wrong.
    - Native then audits every binary ariza put into the bundle and fails
      the build if any of them would load a library from outside it:
      otool -L on Mach-O, readelf -d on ELF, a presence check on PE. The
      verdict functions are pure and take tool output as text, and the
      tool call itself is an injectable seam, so every branch — including
      the Linux and Windows verdicts — is testable from any machine.
    - App::Ariza::Launcher renders bin/ from a Jinja2 template
      (plus the .ps1 and .cmd twins for Windows, CRLF). It resolves its
      own path through a readlink loop rather than `readlink -f`, which
      is a GNU extension a bundle cannot rely on; exports RAKULIB and
      NOTCURSES_NATIVE_DATA_DIR (never NOTCURSES_NATIVE_LIB_DIR, which
      suppresses the terminfo setup) and unsets PERL6LIB; adds
      LD_LIBRARY_PATH and DBIISH_SQLCIPHER_LIB on Linux only; prints a
      one-line first-run notice, once, guarded by a sentinel outside the
      bundle; warns without refusing when TERM is dumb or absent; and
      quotes every expansion, so a bundle unpacked into a path with
      spaces works. Golden-file tests cover all three platform slugs.
    - App::Ariza::Bundle orchestrates the four, then writes VERSION (one
      screen: app version and component pins), ariza-manifest.json (the
      same plus every source URL, every sha256, every installed
      distribution and the smoke commands) and LICENSES/ — the app's
      licence, Rakudo's, and a COMPONENTS.md whose native inventory is
      read off the staged libraries rather than written down, including
      an explicit note that this notcurses pack's FFmpeg is a GPL build.
      Building a platform the app does not declare in bundle.platforms is
      an error.
    - App::Ariza::Smoke unpacks a finished archive into a scratch
      directory with a space in its name and runs it with a *replaced*
      environment — PATH, HOME, TERM and nothing else — so a bundle
      cannot pass by quietly using the developer's system Rakudo or their
      DBIISH_SQLCIPHER_LIB. It re-runs the native audit over the unpacked
      tree and reports pass or fail per check, keeping the tree on
      failure. Smoke commands come from the bundle's own manifest, so an
      archive can be checked with no access to the app's repository.
    - bundle.smoke now accepts a list, and each entry may be argv rather
      than a command line, so a smoke test can carry an entire program
      with no shell and no quoting layer. Placeholders: {exec}, {raku},
      {site}, {native}, {bundle}, {tmp}. App-Moneymoor uses this to
      smoke-test the thing --version never touches: creating, writing and
      reopening an encrypted database from inside the bundle.
    - App::Ariza::Tools is the new shell-out layer — run, download,
      digest, extract — so no other module contains a bare `run` and
      every failure reads the same way. No new dependencies: ariza
      fetches three artefacts from static hosts with curl, exactly as
      every Build.rakumod in this tree already does.
    - Fixed: current-slug() and detect-slug() died with a type-check
      failure when ARIZA_PLATFORM was unset, because %*ENV returns Any
      for a missing key and the parameter is typed Str. That is every
      machine that has never heard of the variable.
    - Initial release. Phase one of ariza: the distribution scaffold and
      the shared machinery a bundler needs, with no bundling yet and no
      placeholder pretending otherwise — three verbs, all of which do
      their whole job.
    - App::Ariza::Platform names the platform an artefact is built for.
      The eight slugs (macos-arm64, macos-x86_64, linux-{x86_64,aarch64}-
      {glibc,musl}, windows-{x86_64,arm64}) are Notcurses-Native's,
      character for character, because a bundle carries that
      distribution's prebuilt libraries and the two have to agree about
      what platform they are on. Detection probes $*KERNEL plus, on
      Linux, the libc: a ld-musl-*.so.1 loader first, then ldd --version
      for glibc. ARIZA_PLATFORM forces a slug for cross-builds and CI
      matrices, and is validated against the known set, because an
      override is the one input that is definitely a human mistake if it
      is wrong.
    - App::Ariza::Versions parses resources/versions.toml, the single pin
      file every artefact is built against. A new [rakudo] section
      (version 2026.07, revision 01) names the runtime a bundle embeds,
      which is deliberately not the same pin as rakudo-star: that one is
      the release an installer tells a package manager to fetch onto the
      user's machine.
    - App::Ariza::Config parses the per-app ariza.toml manifest, which
      lives in the app's own repository so ariza never grows a list of
      special cases about specific apps. [app] name/exec/display are
      required; [bundle] declares platforms, native libraries and a smoke
      command whose {exec} expands to the launcher name.
    - Unknown keys warn and never die, in both config files, so one
      manifest can serve several ariza versions; wrong types die naming
      the dotted path; //-prefixed keys are ignored. The one exception is
      an unknown platform slug in bundle.platforms, which dies: the
      supported set is closed, so a typo cannot be a future feature, and
      ignoring it would ship a release quietly missing a platform.
    - App::Ariza::Legacy absorbs installers/generate.raku intact — the
      Jinja2 templates, the per-app configs, the sqlcipher mirror notes
      and the pin file are all ariza resources now. It renders the eight
      install/uninstall scripts per app, defaulting to each app's own
      repository exactly as the old script did, with an --out-dir
      override so a render can be inspected before it lands.
    - The absorption is gated by a byte-for-byte parity test: all eight
      App-Mindmoor scripts are rendered to a scratch directory and
      compared against the copies committed in that repository, with a
      unified diff excerpt on any mismatch. Drift in a template, a
      context key or the Jinja2 dependency fails the suite instead of
      reaching a user.
    - App::Ariza::Resources finds ariza's own data through %?RESOURCES
      when installed and the checkout tree when not, including directory
      listing, which %?RESOURCES cannot do — installed runs enumerate the
      distribution manifest, checkouts read the working tree.
    - bin/ariza: render-legacy [--app] [--out-dir], version, help.
    - App-Moneymoor gains the first ariza.toml.