promotional bannermobile promotional banner

Ka0s Panel Master

Draws plain backdrop panels behind your UI, so a screen full of separate frames reads as a few deliberate groups
Back to Files

1.0.0-release

File namePanelMaster-1.0.0-release.zip
Uploaded
Aug 7, 2026
Downloads
5
Size
103.1 MB
Flavors
Retail
File ID
8594053
Type
R
Release
Supported game versions
  • 12.0.7

What's new

commit f83a5a9c35515031a59f6deaed7a7a489f5c08f1
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Aug 7 16:07:35 2026 +0530

    release: 1.0.0 — the addon's first published version

    0.1.0 was never released. It was set in the initial commit and never moved
    across all 108 commits, no tag has ever existed, and CurseForge reports no
    file for project 1642836. So this is not an upgrade from 0.1.0 — it is the
    first thing anyone can install, and the docs now say that rather than
    implying a predecessor.

    Release gate (automated-tests-§3), read from the manifest the run wrote,
    before any file here was edited:

      lint        pass  0 warnings / 0 errors in 25 files
      tests       pass  717 passed, 0 failed
      perf        skip  no tests/perf.lua — the no-scenarios exception
      complexity  pass  0 functions above CCN 15
      verdict     green

    PERF WAS NOT MEASURED. The gate's narrow exception covers a skip caused by
    there being no scenarios to run, which is this one — not a missing
    interpreter, and not a ratified performance-§12 exemption, which this addon
    does not qualify for (modules/Canvas.lua:577 runs a shared 10Hz OnUpdate,
    failing criterion (a)). 1.0.0 therefore ships verified against three suites,
    not four, and the README's Version History row says so in as many words.

    Version History, not "What's new". The 0.1.0 row described a release that
    does not exist — "First release", with an em dash where its date should be
    — so it is REPLACED by a 1.0.0 row rather than joined by one, on the owner's
    explicit call, since a table naming two first releases helps nobody. No
    `## What's new` section is written: with nothing published before it, it
    could only restate the feature list directly above it. The
    documentation-§1 (item 5) deviation row stands, and its re-check trigger is
    corrected — it read "the first version bump after 0.1.0", which fired on
    this release, the one release that by definition has nothing to report. It
    now names the first release AFTER 1.0.0.

    Version strings rolled: the TOC, NS.version, the five test expectations that
    would otherwise fail, and the two smoke-test steps that quote `/pm version`.
    Also rolled: the docs and comments asserting what the SHIPPING version does
    — "0.1.0 ships English-only", the events-frames-taint-§8 decline, the
    localization-§1 row — because those are live positions, not dated decisions,
    and a claim about a superseded version reads as stale rather than historical.

    Deliberately not rolled: CLAUDE.md's `Bundles [LibKa0s] … v1.8.3` (the
    vendored library's tag and the vendor gate's input, not the addon's version)
    and media/poster/artwork-poster.txt (generated; it records the state at
    generation time and moves when the poster is regenerated).

    The release bundle is committed with the release, so the tagged tree carries
    the evidence for itself. Its ANALYSIS.md and the RESULTS.md watch list carry
    two things forward that the green columns hide: max CCN has been exactly 15
    for four runs with the gate set above 15, so R.ApplyArtSize and
    Compat.AddOnFolders sit on the line and one added branch in either refuses
    the next release; and settings/PanelEditor.lua moved for the first time in
    four runs, 1064 to 1091. The automated-tests-§4 shelf-life clock starts here
    for all three band entries, this being the first release run they can be
    counted against.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01Qp3u6v8mFe6iTEiwDPWutP

commit 91de582a9d2aa75ff1c4099559b102d546d4364b
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Aug 7 15:50:40 2026 +0530

    docs(media): record why two assets carry a JPEG twin, and file the screenshots

    `e0a868f`'s message called the screenshot set "mid-conversion, not a
    settled convention" and flagged the mixed formats as something to decide.
    That reading was wrong and this corrects it: the convention is settled and
    the mix is the convention working.

    PNG is the repo default — lossless, re-derivable, what a capture or a
    render is kept as. CurseForge caps an uploaded image at 2 MB, and exactly
    two assets exceed it: the 3439x1439 hero capture (8.1 MB) and the artwork
    poster (2.1 MB). Each carries a JPEG twin compressed only to clear that
    limit, and it is the twin the README links, because the README points at
    the CDN copies rather than at repo paths — CurseForge serves the project
    page from its own CDN, so a repo-relative path renders on GitHub and
    nowhere else. Screenshots 01 to 03 are under the cap and stay PNG on both
    sides. The format follows the file size.

    So the rule for a future capture is not "convert it": under 2 MB it needs
    no JPEG, over it gets one, and the PNG stays as the master either way.

    Also documents media/screenshots/ itself, which nothing under docs/
    mentioned, and notes on the poster that its --check fingerprint is the
    PNG's — the JPEG is a lossy derivative and could not round-trip a
    pixel-level comparison.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01Qp3u6v8mFe6iTEiwDPWutP

commit e7850accfabcd8ebcf9ba45422f9624d88f9b543
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Aug 7 15:45:48 2026 +0530

    docs: settle the two release deviations and cover the profile-switch checks

    **Screenshots row retired.** `documentation-§1` (item 6) said the README's
    `## Screenshots` section carried a placeholder because the addon was
    unpublished; its re-check trigger was the first published release filling
    the section. That has happened — four captures are on the CurseForge CDN,
    embedded in the README, and their sources committed in e0a868f — so the
    row goes rather than being kept for the history, per this section's own
    "not a graveyard" rule. Issue #1 is closed as `[done]` with the detail.

    **What's-new row added.** `documentation-§1` (item 5) requires a
    `## What's new` section and the README no longer has one. Ratified as a
    deviation rather than restored, on the owner's decision: 0.1.0 is the
    first release, so there is nothing new to report — the section exists to
    tell a returning player what changed since the build they had, and on an
    initial release every reader is a new one. It returns at the first update,
    which is the first moment it can say something true, and
    `/wow-addon:bump-version` rolls it with `## Version History` anyway — so
    that command is the re-check trigger and retires the row.

    **Smoke tests.** 12b.7 already asked for the panel dropdown to list the
    new profile's panels after a switch. That check is older than the bug it
    catches and had gone unrun, which is how the defect shipped; it now also
    names the Copy-settings-from list and the open editor, and says why one
    rebuild covers all three.

    New 12b-2 covers what 12b.7 never did: the session state keyed by panel
    id. Ids are per profile, so preview's tracked ids, the preview flag, the
    per-panel unlocks and the combat-queued unlocks all named someone else's
    panel after a switch. Step 3 is the destructive one — preview on, switch,
    preview off used to delete the real panels of the profile you switched to.
    All six verified in a live client before this commit.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01Qp3u6v8mFe6iTEiwDPWutP

commit e0a868f0e43270e52635872dbe3c3aa473873e2c
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Aug 7 15:33:41 2026 +0530

    assets: commit the project-page screenshots and the poster JPEG

    The four screenshots behind README's `## Screenshots` section, plus the
    JPEG render of the artwork poster the artwork section embeds. Both are
    already live on the CurseForge CDN and the README points at those URLs,
    so this commits the sources rather than changing what any reader sees.

    None of it ships to players. `.pkgmeta` ignores `media/screenshots`
    wholesale and `media/poster` likewise: WoW cannot load a .png or .jpg at
    all, so shipping them would add megabytes to every download for files the
    client physically cannot use. They are committed for the same reason the
    logo renders and the raw artwork plates are — so an asset can be
    re-derived without going back to whoever produced it.

    Two things a later reader should not have to work out:

    - `panelmaster.screenshot.04` is here TWICE, as an 8.1 MB .png and a
      1.9 MB .jpg of the identical 3439x1439 image. That is the master/render
      pair the logo already uses, but only 04 has been converted — 01 to 03
      are .png only, and the README serves 01 to 03 as PNG and 04 as JPEG to
      match. So this is a mid-conversion set, not a finished convention.
    - Consequently 14 MB lands in git history permanently. Worth a decision
      before the set grows: either convert 01 to 03 and keep the PNGs as
      masters, or drop the PNG masters for screenshots specifically, which
      are re-takeable from the client in a way the artwork plates are not.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01Qp3u6v8mFe6iTEiwDPWutP

commit 8fc3d38378df30d13556be44cc993c99e2902d6d
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Aug 7 15:33:20 2026 +0530

    docs(readme): close the hero screenshot's image link

    The URL was split across a newline before the closing paren, so the
    markdown never formed an image — it rendered as literal text on GitHub,
    and CurseForge's stricter renderer would not have done better. It is the
    first thing a visitor sees on both surfaces. The other three screenshots
    were already well formed.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01Qp3u6v8mFe6iTEiwDPWutP

commit c596092269bd90da8dd107a223e3833a66186c1d
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Aug 7 15:33:11 2026 +0530

    refactor(panel): drop P:RefreshPanels, now that nothing calls it

    Its one caller was settings/PanelEditor.lua's MSG_PANEL handler, which
    now calls the library's O.RefreshPanel(ctx, false) instead — the same
    refresher run, but the shown/hidden decision and the dirty flag belong to
    LibKa0s rather than being hand-rolled per host. Nothing else ever called
    it: no other module, no slash verb, no test.

    P:Refresh stays. It is the same shape but genuinely live — the console
    checkbox mirroring the debug window calls it by name, and it is still
    worth having beside O.RefreshScalars for the reason the comment above it
    gives: the library's version sweeps every registered ctx, which would
    refresh three pages to repaint one.

    The comment block above the survivor said "these two"; it now says what
    happened to the other one, so the next reader does not go looking for a
    symmetry that has been removed on purpose.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01Qp3u6v8mFe6iTEiwDPWutP

commit 630d1ea6a47459d36576ca551c8940ae12c11fc2
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Aug 7 15:26:11 2026 +0530

    fix(profiles): scope the Panels page and session state to the active profile

    Switching or creating a profile hid the panels from the screen but left
    them in Settings > Panels > the Edit Panel dropdown, the copy-from list
    and the open editor. Four bugs, one family: state keyed to a profile that
    outlived the switch. Panel STORAGE was never the problem — panels and
    nextID have always lived in db.profile, and every Registry entry point
    re-reads it live.

    1. The page never re-rendered. settings/PanelEditor.lua hand-rolled the
       hidden-page branch and marked `ctx.dirty`; LibKa0s's SetRenderer OnShow
       gate reads `ctx._dirty`. The flag was written in four places and read
       in none, so the deferred repaint was dropped. A profile switch happens
       from the Profiles page, i.e. with the Panels page hidden, so the page
       kept the widget tree it built for the previous profile. Canvas is not a
       settings page, never consulted the flag, and correctly cleared the
       screen — which is exactly the split the report described.

       Fixed upstream rather than by correcting the flag name: a private field
       the host has to guess IS the defect. LibKa0s v1.8.3 publishes
       O.RefreshPanel(ctx, structural), which owns the shown/hidden decision,
       the flag and both tiers. Both handlers are now one call to it and the
       host-owned `dirty` field is deleted outright, so there is no second
       copy of the rule to drift.

    2. Preview could delete real panels. nextID is per profile, so every
       profile issues ids from 1. NS.State.previewIDs survived a switch and
       `/pm preview off` called DeleteBatch with the OUTGOING profile's ids,
       which resolve against the current one: preview on, switch, preview off
       destroyed the panels the user had made in the profile they switched to.
       NS.State.preview also stayed true, so preview could not be restarted to
       clear it.

    3. Inherited unlocks. NS.State.unlockedPanels and Unlock's deferred
       pendingPanels survived the same way, so a panel in the incoming profile
       came up individually unlocked wearing a drag handle nobody asked for.

    4. Silent wrong selection. PanelEditor's selectedID survived, and the
       rebuilder's Registry:Get(selectedID) check cannot tell that the id now
       names a different panel, so the editor opened on a panel nobody chose.
       Only observable once (1) was fixed.

    2-4 are one fix: Registry:ReloadProfile now drops every session table
    keyed by panel id BEFORE it sanitizes or broadcasts, via new
    Unlock:ForgetPending and PanelEditor:ForgetSelection seams. Same sweep
    R:DeleteAll already does, for the same reason. The global unlock flag is
    deliberately kept — it is a mode the user put the screen in, not a claim
    about a panel.

    Re-vendored libs/LibKa0s from v1.8.3 (byte-identical) and moved the
    CLAUDE.md provenance line in the same commit, as the vendor gate requires.

    Also in here, found while verifying: PanelMaster.toc gains
    X-Curse-Project-ID 1642836 in the slot toc-file-§1 pins, and the matching
    toc-file-§1 deviation row retires from docs/ARCHITECTURE.md now that the
    project id exists.

    Tests 713 -> 717. The three new profile-switch cases were verified to fail
    against the unfixed ReloadProfile. tests/test_panel.lua's dirty case
    asserted the WRONG flag name, which is how this shipped green; it now
    asserts the library's flag and a second case checks the repaint actually
    lands on the show. tests/test_libka0s.lua's widgetsOfType counted every
    widget the whole run had ever created — correct only while the Panels page
    never built itself in the harness, which stopped being true here — and is
    now scoped to the window the General page's render opened.

    Gate: 717/717, luacheck 0/0 in 25 files, vendor diff empty on both
    readings.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01Qp3u6v8mFe6iTEiwDPWutP

commit d71fa72facbc173b918ae227df4dc56536883908
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Aug 7 13:59:55 2026 +0530

    docs: track the in-game capture store's rename to docs/perf-analysis/

    The standard retired the flat `docs/perf-runs/` store and replaced it with
    `docs/perf-analysis/`, where each capture is a frozen dated bundle holding
    the record and its reading together. This addon ships neither store and
    still ships none: it declines `LibKa0s-Perf`, registers no `perf` verb and
    has nothing to capture, so the four live docs that named the old directory
    only needed the new name.

    Nothing about the position changed. `docs/performance.md` still says the
    `performance-§12` no-combat-path exemption is not claimable here — the
    mouseover `OnUpdate` at `modules/Canvas.lua:577` runs in combat — and the
    sweep behind that argument is untouched. The frozen audit and review
    bundles keep their original wording, as frozen history should.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01FgcWFzX8WzaWuDHSU9kqZp

commit 090dc5d565f16d2a8b4bcda6997df996ffc97e02
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Aug 7 12:08:35 2026 +0530

    automated-tests: record run 20260807-114409 — green

    Four-suite run through the vendored kit (LibKa0s v1.8.2, test-kit
    revision 10). lint 0 warnings / 0 errors over 25 files; tests
    713 passed, 0 failed, 0 skipped; perf skipped — no tests/perf.lua;
    complexity 0 warnings, max CCN 15 (recorded, non-gating).

    First bundle written since kit r10's normalize_eol pass landed, so it
    is also the evidence that a run no longer leaves LF stragglers in a
    CRLF-pinned repo: every file in the bundle has CR count == LF count.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01LE5Tjrv46E2iZfFVS47WmK

commit 69ceb3cbe3f8119d22c833e1ab43c7e69fbc0a9a
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Aug 7 11:41:01 2026 +0530

    chore(gitattributes): mark the vendored upscaler binary, as a ratified deviation

    `tools/artwork/bin/realesrgan-ncnn-vulkan` is an extensionless ELF
    executable, so no `*.ext` rule in the canonical binary list can reach it.
    Unmarked, it was the one working-tree stray line-endings-§7 reported for
    this addon — a genuine §4 failure, not a false positive of the old check.

    Marking it by path means .gitattributes is no longer byte-identical to
    the canonical body, which §5 requires, so the two MUSTs collide. Records
    it as a ratified row in ARCHITECTURE.md's deviation register rather than
    leaving an audit to re-file it every cycle; the row retires if §4/§5 gain
    a provision for extensionless binaries upstream.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01LE5Tjrv46E2iZfFVS47WmK

commit b6e7db8753be6d8499d4f4d67949be7a6d1d0fda
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Aug 7 11:37:54 2026 +0530

    chore(gitattributes): re-sync to the canonical body updated in v2.28.1

    line-endings-§5 requires .gitattributes to match the canonical body
    byte-for-byte. Correcting §7's working-tree check also rewrote the
    verification advice inside that body — `file <path>` cannot report line
    terminators for JSON or for a binary, so it passes files it never
    examined — and no repo was re-synced when that landed.

    Comment text only; no rule, pin or carve-out changes.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01LE5Tjrv46E2iZfFVS47WmK

commit 43e6e228fab19e36c9d25ad83a74653e22272c7d
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Aug 7 11:07:18 2026 +0530

    vendor: re-vendor LibKa0s v1.8.2 — test kit revision 10

    Both payloads move together, from the tag rather than master, in the same
    commit as the provenance line — which is what the vendor gate compares
    against and what the last attempt got wrong.

    - libs/LibKa0s/ and tests/_kit/ taken wholesale from LibKa0s v1.8.2
      (ac857fd); all four diffs against the sibling checkout are empty, both
      content and bytes.
    - Kit.VERSION 9 -> 10. run-automated-tests.sh now reads .gitattributes per
      path with `git check-attr text eol` and writes the bundle in the endings
      the repo declares, instead of leaving LF stragglers that git status never
      mentions and --renormalize never fixes.
    - CLAUDE.md provenance line 1.8.1 -> 1.8.2.
    - Options.lua carries an options-ui section-reference correction.

    Green: 713 passed / 0 failed / 713 total, luacheck 0/0 in 25 files, and
    both tests/test_vendor_sync.lua cases PASS rather than SKIP.

    The 20260807-110543 bundle is the first written by the fixed runner and the
    proof it works: every file lands CRLF on disk, CR count equal to LF count.

commit 90d997460b9c8b36f22ee09bbf8784062a1b3f72
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Aug 7 03:22:56 2026 +0530

    docs: land the deviation register and the full docs/ set in CLAUDE.md

    Two sentences in root CLAUDE.md had drifted from the upstream standard
    (v2.28.0), and both are read as instructions by every agent that opens
    the repo:

    - "Standards compliance (read first)" item 1 still told the agent to
      record an accepted deviation "in the TOC/README/docs/ and in the audit
      bundle". documentation-§3 has since made docs/ARCHITECTURE.md ->
      "## Documented deviations" the single home, with a fixed row shape and
      a filename-§N Rule cell; a deviation outside that register is not
      ratified, so the old wording sent the decision somewhere an audit does
      not look and got it re-filed as an open MUST every cycle.
    - "The canonical docs/ set is exactly three files" was a count with an
      open slot, which is exactly the shape that invites a model to
      reconstruct a member from memory. It now names the trio, the
      verification-and-record docs, the six Tier 1 topic docs, and points at
      the Documentation map for the conditional ones.

    Reference only; no rule text from the standard was copied in and no
    version stamp was written.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01LE5Tjrv46E2iZfFVS47WmK

commit 04a7f1a2c0795c52f9815254631c8b4df180a75e
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Aug 7 02:35:49 2026 +0530

    automated-tests: record 20260807-023000 — 713/713 green on master

    The recorded run was three commits stale against the tree: the newest bundle
    was taken on feat/fix-ccn at 645868a, before the audit-remediation merge, the
    .gitattributes pin, the documentation tier model and the kit rev-9 re-vendor
    landed. This is the first four-suite run on master since.

    Full default run, no --suite, no --no-bundle. lint 0/0 over 25 files, tests
    713/713 with zero skipped, perf skip (this addon ships no tests/perf.lua),
    complexity 0 warnings over 1352 functions with max CCN still 15. Verdict green.

    The suite is up seven net — eleven cases added, four collapsed or rewritten —
    all of it harness and seam-parity cover following the re-vendor, none of it
    behavioural. Complexity is flat where it matters: avg CCN 2.0 and max CCN 15
    unchanged, both 15s still R.ApplyArtSize and Compat.AddOnFolders. The +56 NLOC
    is entirely in tests/.

    RESULTS.md's four standing sections are rewritten against this bundle. The
    Lint section carried a false claim worth calling out: it said luacheck runs
    over "the addon's own source and its tests/", but .luacheckrc excludes tests/,
    so roughly 6,000 lines of harness are unlinted and the 0/0 row could never
    show it. automated-tests-§4 asks specifically for what the config excludes;
    it now says so.

    Every figure comes from this bundle's manifest.json or a suite artifact.
    Nothing was hand-written and no frozen bundle was touched.

    Also swept the comment-citation check over the repo's own .lua/.toc/.pkgmeta
    (excluding libs/ and tests/_kit/). Nothing was applied: every surviving hit is
    deliberate history (modules/DebugLog.lua, docs/pending/LEDGER.md — both
    correct as past-tense prose), a citation into third-party SunnArt source, or a
    stale rationale rather than a bad path. Details in the run report.

commit d91615f2da5485b947babb35c6c0f16db83b695e
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Aug 7 02:13:12 2026 +0530

    docs: land every cross-reference and file:line citation on what it names

    A pointer that resolves to nothing is read as "the answer is elsewhere" and
    the reader stops looking, so each of these cost a lookup rather than merely
    being untidy.

    - CLAUDE.md sent the seam question to `docs/ARCHITECTURE.md` > *The LibKa0s
      seams*, a section that does not exist; the seam table and the TOC order it
      pins are in `docs/module-map.md`. Its release note pointed at
      `docs/testing.md` > *The complexity report*, retired with the standalone
      report in standard v2.19.0 -- now *Automated test records*.
    - ARCHITECTURE.md's module-map summary put all four LibKa0s seams in `core/`.
      Two are: `CoreSetup` and `DebugLogSetup`. `Slash` and `OptionsSetup` are
      under `settings/`, which the same sentence already half-said.
    - smoke-tests.md's LSM-dropdown step cited `ARCHITECTURE.md` > Options UI;
      the AceGUI value-push workaround is `settings-panel.md` > *Three widget
      workarounds*.
    - automated-tests/README.md linked `../perf-runs/`, which has never existed
      here -- this addon declines `LibKa0s-Perf` and registers no `perf` verb, so
      the link now says that instead of dangling.
    - DEPENDENCIES.md's kit citations had drifted with the re-vendor:
      vendor_sync.lua:140/:139 -> :154/:153 (the `git -C` shell-out and its
      io.popen guard), :96 -> :104 (`localNames`), and make_poster.py:387-389 ->
      :386-388 (the toolchain tuple).

    Green gate: 713/713 tests, luacheck 0/0 over 25 files.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01QuCbofydwVeUp61YW4DiJ4

commit a8e7a2a471a7a067b8a223feddba6a512098cc93
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Aug 6 23:14:08 2026 +0530

    docs: collapse the duplicate ISS-01 citation onto PLAN-05's issue

    ISS-01 and PLAN-05 are the same item — ISS-01's own rationale says so and
    names #1 as the single tracker. The migration briefly gave ISS-01 its own
    issue (#32), which duplicated #1 and carried nothing #1 did not; #32 has
    been deleted, so this citation now points only at #1.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01QuCbofydwVeUp61YW4DiJ4

commit be6d8749bd683ace8b5bf304b86bb75d5837d4db
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Aug 6 21:21:35 2026 +0530

    docs: retire docs/pending/LEDGER.md; the pending store is GitHub issues

    Implements Ka0s WoW Addon Standard v2.26.0. The decision record lived in
    the working tree, which is the one place a decision record cannot be
    reached from: a deferral was invisible to the issue list the maintainer
    actually opens, nothing notified on it, and nothing outside this repo
    could reference it.

    Every row anything still refers to is now a GitHub issue on this repo,
    with status carried as a title prefix — [deferred] and [untriaged] open,
    [done] and [will-not-do] closed. Deferrals and reasoned refusals migrated;
    finished work did not, except where live documentation cites it by id,
    because the pointer is the thing being preserved.

    Every doc and comment reference now points at its issue rather than at a
    deleted file. The terminal rows the file held remain in git history at
    this commit's parent.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01QuCbofydwVeUp61YW4DiJ4

commit fcdacda2375baa194b2c455c10bdcad873754195
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Aug 6 18:39:54 2026 +0530

    docs: move the LibKa0s provenance line to CLAUDE.md; re-vendor kit rev 9

    Adopts LibKa0s v1.8.1, whose only change is test kit revision 9:
    `vendor_sync.lua` now reads the `Bundles [LibKa0s](...) vX.Y.Z (MIT).`
    provenance line out of the consuming repo's CLAUDE.md instead of its
    README.md, with no fallback. The line, the vendored bytes and the
    regenerated inventory therefore all move in this one commit — a
    provenance line and a payload that disagree is the drift the gate exists
    to catch, so they cannot land separately.

    Why CLAUDE.md: "which LibKa0s does this build carry?" is a maintainer's
    question, and README.md is the player-facing page. This repo's README was
    rewritten for players on 2026-08-05; the library inventory was the last
    contributor-facing thing left in it, and the gate was reading its input
    out of a file whose job is to stop mentioning it.

    README.md
    - `## Credits and libraries` becomes a plain `## Credits`. The LibKa0s
      sentence and the Ace3 / LibStub / CallbackHandler / LibSharedMedia
      inventory are gone; the warcraft.wiki.gg artwork attribution stays,
      because that credit is genuinely external and CC BY-SA 4.0 requires it.
    - The Standard badge loses its link wrapper. The badge markdown itself
      is unchanged.

    CLAUDE.md gains the provenance line naming v1.8.1, in the paragraph that
    already describes what this addon vendors and where, with a note that it
    is an input to the gate rather than a remark.

    tests/_kit/ re-vendored whole-folder from the v1.8.1 tag; `diff -r
    --strip-trailing-cr` against the tag is empty. `libs/LibKa0s/` is NOT
    recopied — no shipped library file moved in v1.8.1, so the existing copy
    is already byte-identical, and both payloads are compared against the tag
    the line now names.

    docs/test-cases.md regenerated: kit revision 9 renamed one case to
    "libs/LibKa0s is the LibKa0s release CLAUDE.md says this addon bundles".
    A rename is not a count change — still 713/713, so the README [tests]
    badge does not move.

    docs/testing.md's fifth vendor check and tests/test_vendor_sync.lua's
    header prose now name CLAUDE.md. docs/pending/LEDGER.md ▸ LIBKA0S-32
    records that its "README.md names the bundled version" line is superseded.

    Green: `lua tests/run.lua` 713 passed, 0 failed, 0 skipped, 713 total;
    `luacheck .` 0 warnings / 0 errors in 25 files.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01Y4FjrPatWaye1gFsfS4vpb

commit 7287642fb4410584b343ea792f64e22afb1e82c6
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Aug 6 17:23:43 2026 +0530

    Pin line endings with the canonical .gitattributes (line-endings-§2)

    This is the commit LIBKA0S-09 was deferred for. PanelMaster carried a
    five-line `.gitattributes` holding the `*.sh` carve-out and nothing else —
    the exception with no rule — while its tracked text sat LF against the
    collection's evident intent. The canonical client-bound body replaces it.

    `git add --renormalize .` moved 75 blobs to LF storage and the working-tree
    stragglers were re-materialized from the index. Two source-scanning cases in
    tests/test_panel.lua anchored on "\nend\n" and "\n\n" and went red on the
    flip; both slurp helpers now CR-strip, the same normalization
    tests/_kit/vendor_sync.lua already does. test_vendor_sync.lua's header no
    longer claims the tree is unpinned, and the ledger row closes.

    713 tests pass unchanged.
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01A9nTGYaHsqCEf6obganps6

commit 16fea6d6213d65b63026f99588f441f096425bc2
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Aug 6 16:00:06 2026 +0530

    docs: adopt the documentation tier model (standard v2.23.0)

    ARCHITECTURE.md was 1071 lines — the largest hub in the collection, and the
    case documentation-§3's new spill rule was written against. Nothing required
    the doc set to split, so every subject landed in one file: the artwork
    pipeline, the Sunn adapter, the frame ladder, the widget workarounds and the
    data model all lived there with no landmark between them.

    The hub goes 1071 -> 180 lines, and no mandated section now exceeds 60. The
    content is carved out, not rewritten.

    Created (Tier 1): scope, module-map, schema, settings-panel, data-flow,
    common-tasks. Created (Tier 2, triggers fired): slash-dispatch (17 verbs,
    threshold 8), profiles (AceDB profiles are user-visible — the Profiles page),
    debug (D:Diagnose and NS.DebugBuild are the addon's own beyond the library
    console). Created (Tier 3): rendering, localization, media, alongside the
    existing artwork-spec.

    Four Tier 2 docs are recorded as "Not applicable" rows in the new
    `## Documentation map` with the trigger that would re-arm each. midnight-quirks
    is the one worth stating: core/LSMPatch.lua fixes a vendored WIDGET, not a
    client behavior, so it is not a client-version workaround and belongs in
    module-map.md — filing it as a quirk would misdescribe what it is.

    Verified: 713 tests passing, luacheck clean, every relative doc link resolves.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01KQxehYAMgZtvAJDCJxXYRs

commit 2957283e1d1992ab8db97b0470862ac22805bea9
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Aug 6 01:11:42 2026 +0530

    docs: the four file:line citations this changeset moved now land on the line they name

    Doc-content sync only — no document created, moved or restructured, and no
    generated doc touched. Four citations drifted off their site during the
    remediation batch; each is re-derived from the current tree rather than nudged
    by eye.

    The OnUpdate install site. CLAUDE.md, docs/ARCHITECTURE.md ▸ Documented
    deviations and docs/performance.md (prose plus the hot-path table) all name
    `modules/Canvas.lua:565` as the line that installs the shared 10Hz mouseover
    driver — the whole basis for this addon NOT qualifying for performance-§12's
    no-combat-path exemption. 565 is now `f:SetAlpha(spec.alpha)`, a line inside
    `updateMouseover`; the `mouseoverDriver:SetScript("OnUpdate", ...)` the argument
    actually rests on is at 577. The claim is unchanged and stays true; only the
    pointer moves. A reader checking the deviation register against the code is
    exactly the reader this citation exists for, and 565 sent them to the wrong
    statement.

    DEPENDENCIES.md's two toolchain rows. M3-01/M3-02 emptied
    tests/test_vendor_sync.lua down to registration — 41 lines, the ~140
    hand-copied ones having moved into the vendored tests/_kit/vendor_sync.lua — so
    the `git` row's `tests/test_vendor_sync.lua:53` and its `:52` io.popen guard,
    and the POSIX-shell row's `tests/test_vendor_sync.lua:89`, all pointed past the
    end of the file. The shell row's other half, `tests/test_harness.lua:30`,
    described an `ls tests/test_*.lua` that is now the kit's `listDir` behind
    `T.assertSuiteInventory`. Both rows now cite `tests/_kit/vendor_sync.lua:140`
    / `:139` / `:96` and `tests/_kit/framework.lua:202`, with the LuaFileSystem
    rationale pointed at `framework.lua:197-201` where it now lives. The git row
    says out loud that the shell-out moved into the kit while the requirement did
    not: a dependency whose only evidence sits in a vendored file is still a
    dependency, and a reader who greps the named test, finds three lines of
    `dofile`, and concludes git is no longer needed would be wrong.

    Verified in sync and left alone: docs/test-cases.md is byte-identical to
    `lua5.1 tests/run.lua --list`; the README test badge reads 713/713 and the
    Totals table says 713; NS.COMMANDS carries 18 entries and the README command
    table carries the same 18, both directions; the three bus messages, their
    senders and their consumers match modules/Registry.lua and settings/Schema.lua;
    139 exported functions, none without a caller.

    Verification: 713 passed, 0 failed, 0 skipped; luacheck 0 warnings / 0 errors in
    25 files; lizard 1352 functions, avg CCN 2.0, 0 above threshold; all four
    vendor-drift diffs against ../LibKa0s empty, content and byte.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01KQxehYAMgZtvAJDCJxXYRs

commit 2957283e1d1992ab8db97b0470862ac22805bea9
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Aug 6 01:11:42 2026 +0530

    docs: the four file:line citations this changeset moved now land on the line they name

    Doc-content sync only — no document created, moved or restructured, and no
    generated doc touched. Four citations drifted off their site during the
    remediation batch; each is re-derived from the current tree rather than nudged
    by eye.

    The OnUpdate install site. CLAUDE.md, docs/ARCHITECTURE.md ▸ Documented
    deviations and docs/performance.md (prose plus the hot-path table) all name
    `modules/Canvas.lua:565` as the line that installs the shared 10Hz mouseover
    driver — the whole basis for this addon NOT qualifying for performance-§12's
    no-combat-path exemption. 565 is now `f:SetAlpha(spec.alpha)`, a line inside
    `updateMouseover`; the `mouseoverDriver:SetScript("OnUpdate", ...)` the argument
    actually rests on is at 577. The claim is unchanged and stays true; only the
    pointer moves. A reader checking the deviation register against the code is
    exactly the reader this citation exists for, and 565 sent them to the wrong
    statement.

    DEPENDENCIES.md's two toolchain rows. M3-01/M3-02 emptied
    tests/test_vendor_sync.lua down to registration — 41 lines, the ~140
    hand-copied ones having moved into the vendored tests/_kit/vendor_sync.lua — so
    the `git` row's `tests/test_vendor_sync.lua:53` and its `:52` io.popen guard,
    and the POSIX-shell row's `tests/test_vendor_sync.lua:89`, all pointed past the
    end of the file. The shell row's other half, `tests/test_harness.lua:30`,
    described an `ls tests/test_*.lua` that is now the kit's `listDir` behind
    `T.assertSuiteInventory`. Both rows now cite `tests/_kit/vendor_sync.lua:140`
    / `:139` / `:96` and `tests/_kit/framework.lua:202`, with the LuaFileSystem
    rationale pointed at `framework.lua:197-201` where it now lives. The git row
    says out loud that the shell-out moved into the kit while the requirement did
    not: a dependency whose only evidence sits in a vendored file is still a
    dependency, and a reader who greps the named test, finds three lines of
    `dofile`, and concludes git is no longer needed would be wrong.

    Verified in sync and left alone: docs/test-cases.md is byte-identical to
    `lua5.1 tests/run.lua --list`; the README test badge reads 713/713 and the
    Totals table says 713; NS.COMMANDS carries 18 entries and the README command
    table carries the same 18, both directions; the three bus messages, their
    senders and their consumers match modules/Registry.lua and settings/Schema.lua;
    139 exported functions, none without a caller.

    Verification: 713 passed, 0 failed, 0 skipped; luacheck 0 warnings / 0 errors in
    25 files; lizard 1352 functions, avg CCN 2.0, 0 above threshold; all four
    vendor-drift diffs against ../LibKa0s empty, content and byte.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01KQxehYAMgZtvAJDCJxXYRs

commit 926480e697ad3c675a115fe53a75c48dc15a9c7c
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 23:14:16 2026 +0530

    test(db): observe that the v1 ladder RUNS, not the shape of defaults.global  [fixes M4-22 test]

    "the schema stamp is WRITTEN at init, never served as a default" asserted
    `NS.defaults.global.schemaVersion == nil` — the key the same commit deleted from
    defaults/Global.lua — and the same precondition was repeated inside the
    migration case. core/Database.lua:94 floors the stamp with
    `g.schemaVersion = g.schemaVersion or 1`, so seeding 1 in defaults is
    behaviorally identical to omitting it: the `<` gate still opens, the v1 -> v2
    body still runs, no user can tell the difference — yet both assertions failed.
    Its companion, `NS.db.global.schemaVersion == NS.SCHEMA_VERSION`, said nothing
    about the regression it was written for.

    Both defaults-shape assertions are gone. The migration case at
    tests/test_database.lua:21 already carries the real guarantee — copy
    NS.defaults.global into a fresh global, hand RunMigrations a profile with an
    unstamped panel, assert
    `NS.db.profile.panels[1].frameName == "PanelMaster_Panel_Old_Panel"` — verified
    before deleting. The first case is retargeted to the one thing left that only it
    observes: NS:InitDB invokes the runner (core/Database.lua:19), so the live DB
    comes back stamped.

    Production mutations used to prove them red:
      * defaults/Global.lua seeds `schemaVersion = NS.SCHEMA_VERSION` (the real
        regression): migration case RED — "the v1 -> v2 body did not run for a v1
        SavedVariables file (expected PanelMaster_Panel_Old_Panel, got nil)", 12
        failures in all.
      * defaults/Global.lua seeds `schemaVersion = 1` (correct for the user, and
        what the deleted literals reddened): 713 passed, 0 failed.
      * `NS:RunMigrations()` commented out of NS:InitDB: retargeted case RED — "the
        live DB is unstamped — InitDB did not run the migration runner (expected 2,
        got nil)".
    Reverted: 713 passed, 0 failed.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

commit 1548383632844582250d5c826b0e2d457586d14f
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 23:11:51 2026 +0530

    test(harness): observe the majors the runner actually loaded, not run.lua's own bytes  [fixes M3-03 test]

    "the runner derives the vendored library's load list from LibKa0s.xml" grepped
    tests/run.lua for the literal `Loader.xmlFiles("libs/LibKa0s/LibKa0s.xml")` —
    the exact bytes the same commit added — and then compared one parse of the XML
    against another parse of the same XML. Neither half observes what the harness
    LOADED. Hoisting the path to a local while loading precisely the right eight
    files reddens it with the environment perfect; conversely, loading six of the
    eight files (PM-A-10, which survived a year of green runs) leaves it green.

    The literal grep is relaxed to a real lint — the runner must go through
    Loader.xmlFiles and must not type a `libs/LibKa0s/*.lua` path — and a new case,
    "Harness: every module LibKa0s.xml declares is live in the loaded environment",
    reads the already-built environment instead: LibKa0s-Core-1.0 and
    LibKa0s-Perf-1.0 are registered, Perf.lua published its own minor, PerfPanel.lua
    attached (it has no major of its own, so it is observed through
    lib.MODULES.PerfPanel), and every major found by scanning the XML-declared files
    resolves through LibStub.

    Production mutation used to prove it red: tests/run.lua's load call rewritten to
    feed Loader.loadAll the first six of the eight XML entries — the original defect
    — so Perf.lua and PerfPanel.lua never load. New case RED
    ("LibKa0s-Perf-1.0 did not register — Perf.lua was not loaded"); the old
    parse-versus-parse case and the lint both stayed GREEN, which is the point.
    Reverted: 713 passed, 0 failed.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

commit 429ed3ee007776fac4b1bd9e5db7070e352555b1
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 15:12:24 2026 +0530

    docs(arch): re-grade the pre-formatting sites under the scoped events-frames-taint-§8  [M4-27]

    DISPOSITION, NOT A FIX. M1-STD-11 was resolved as option (a): §8's
    pre-formatting MUST NOT is now scoped to call sites whose arguments are, or
    derive from, a return of a named combat-protected API, and everything outside
    that trigger set is a SHOULD whose stated reason is drift, not secrets. No call
    site is converted here and no cited code is touched.

    Checked whether any of this repo's ~25 sites sits on a named API: none does. A
    whole-repo sweep of core/ modules/ settings/ defaults/ locales/ for the trigger
    set — UnitGetTotalAbsorbs, UnitGetTotalHealAbsorbs, UnitGetIncomingHeals,
    UnitHealth, UnitHealthMax, UnitThreatSituation, UnitDetailedThreatSituation, the
    aura amount/points fields and UNIT_AURA — returns nothing, and the only
    unit/client APIs PanelMaster calls at all are UnitClass and
    C_AddOns.GetAddOnMetadata. So there is no real fix hiding in this half: every
    site formats values the addon owns.

    Both of §8's unrelaxed points were verified rather than assumed. No file calls
    the global print() for user-facing output; every one takes
    `local print = NS.Print`. The seam's guarantee is unconditional:
    core/CoreSetup.lua publishes the library's IsConcatSafe / SafeToString and
    builds the printer from lib:New, so every argument is stringified through the
    table.concat probe whatever a call site hands it.

    Records the re-grade as a register row in docs/ARCHITECTURE.md ▸ Documented
    deviations with its re-check trigger — the first line whose arguments derive
    from a trigger-set API, which converts as a MUST, plus a re-check when §8's
    list grows upstream.

    Closes PM-A-08.

    Verification: luacheck 0 warnings / 0 errors in 25 files; 712 passed, 0 failed;
    lizard Warning cnt 0; git status shows only docs/ARCHITECTURE.md.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

commit 829cb029b991467d6440c37963b8305d9db4bb81
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 15:10:59 2026 +0530

    fix(debug): the degraded console acks in its own words, not the library's line format  [M4-26]

    debug-logging-§7 requires the ack — logging is a session flag the ADDON owns,
    so a degraded install that flipped it silently would look like the flag was
    stuck off. But `core/DebugLogSetup.lua`'s stub produced that ack by
    hand-copying the library's own line: the ACK format "debug logging %s" from
    `libs/LibKa0s/DebugLog.lua`'s STRINGS table and both state hexes it composes at
    SetEnabled (ON green 40ff40, OFF red ff4040).

    That is the transcription this file was created to end (anti-pattern #47), and
    it is a copy that cannot be kept true: the stub only runs when the library is
    ABSENT, so nothing on that path can read lib.STRINGS, and nothing would notice
    the day the library restyles its own ack. The stub now states the same fact in
    its own plain words — "debug logging is on" / "debug logging is off" — and
    explainOnce still says why the window is not there to match. The live path is
    untouched, so a normal install's ack is byte-identical to before.

    Tests: "Degraded install: /pm debug on|off still flips the flag and
    acknowledges" now asserts an ack is actually emitted on each edge, that the two
    edges differ, and that neither line carries either of the library's state hexes.
    Deliberate violation: restoring the hand-copied ack reddened that case
    (711 passed, 1 failed); reverted, green restored.

    Closes PM-R-03.

    Verification: luacheck 0 warnings / 0 errors in 25 files; 712 passed, 0 failed;
    lizard Warning cnt 0; `grep -nE 'cff40ff40|cffff4040|<ts>' core/DebugLogSetup.lua`
    returns nothing (exit 1). Test count unchanged, so docs/test-cases.md is
    unchanged.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

commit febf1084f455756ff2097f1b6a8fd859edfdeeb4
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 15:08:33 2026 +0530

    fix(db): the schema stamp stops being an AceDB default, so the migration bodies are reachable  [M4-22]

    Three PanelMaster one-offs.

    1. savedvariables-§1 (PM-R-04). `defaults/Global.lua` seeded
       `schemaVersion = NS.SCHEMA_VERSION` as an AceDB default. A default is
       served for any key the SavedVariables file does not carry, and
       NS:RunMigrations only WRITES the field from inside its own `<` gate — so
       `db.global.schemaVersion` read as current on every account that had never
       been stamped, i.e. every account, the gate never opened, and the v1 -> v2
       frame-name body was dead code for every real install. The seed is removed;
       the field is written, never defaulted.

       `core/Database.lua`'s profile-callback `NS:RunMigrations()` is removed
       rather than kept: the stamp is ACCOUNT-WIDE and already written by InitDB
       before any profile can be switched, so the call could only ever be a no-op
       dressed as a safety net. The repair an incoming profile actually needs is
       per-RECORD and is R.Sanitize's, reached through Registry:ReloadProfile,
       which performs the same frame-name backfill.

    2. PM-R-06. `modules/Canvas.lua` reaches every sibling directly and unguarded
       in all thirteen places; the review's premise that the majority guard first
       was false. The convention is now recorded in the file header, together with
       why the two `NS.Unlock` overlay reaches are method-presence checks rather
       than load-order guards. No call site changed.

    3. library-stack-§4 (PM-R-12). `settings/OptionsSetup.lua`'s `onAceGUI`
       stashed `NS.AceGUI`, which had no reader anywhere and whose comment claimed
       an effect it did not have. Removed: the library already publishes the handle
       on the instance as `O.AceGUI`. The three `LibStub("AceGUI-3.0", true)` calls
       each resolve the one singleton once, and none of them can be served from a
       build-time seam — Panel.lua and PanelEditor.lua run at file scope before the
       instance exists, LSMPatch.lua inside a one-shot PLAYER_LOGIN fixup. Recorded
       in place.

    Tests: adds "Database.RunMigrations: a v1 SavedVariables file reaches the
    v1 -> v2 body", which drives the runner against a global built from the shipped
    defaults and a profile of unstamped records. Rewrites the two cases the fix
    made false or vacuous. Deliberate violations: re-seeding `schemaVersion` in
    defaults/Global.lua reddened both new database cases (710 passed, 2 failed);
    disabling the Sanitize frame-name backfill reddened the profiles case plus the
    Registry.FrameName case (710 passed, 2 failed). Both reverted, green restored.

    Closes PM-R-04, PM-R-06, PM-R-12.

    Verification: luacheck 0 warnings / 0 errors in 25 files; 712 passed, 0 failed;
    lizard Warning cnt 0; docs/test-cases.md regenerated and CR-normalized diff
    against `--list` empty; README test badge 711 -> 712.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

commit 81269137ad48a6ff85b6cf2e41ac05f55d548407
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 15:01:11 2026 +0530

    docs(arch): record the English-only decision as a register row  [M4-17]

    `localization-§3` names two terminal compliant states for the routing
    SHOULD: strings routed through `NS.L`, or an English-only decision in the
    `## Documented deviations` register with a re-check trigger. PanelMaster
    is in the second state — the decision was reasoned at `locales/enUS.lua:8-14`
    but never ratified in the register, so every audit cycle re-filed it as an
    open localization row.

    Adds the `localization-§1` row: what differs, why 0.1.0 ships English-only,
    that both MUSTs (the exported seam with the key-returning fallback at
    `locales/enUS.lua:6`, and `enUS.lua` shipping) are met unconditionally, and
    the re-check trigger — the first non-English locale file added to `locales/`.
    Carries forward the file's own note that panel names and the stored
    `point` / `strata` tokens must never route through `NS.L`.

    Closes PM-A-17.

    Verification: luacheck 0 warnings / 0 errors in 25 files; 711 passed,
    0 failed; lizard Warning cnt 0; git status shows only docs/ARCHITECTURE.md.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

commit fb626b3a2c92a4a2139ae599ae1414cc99b1df80
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 14:58:31 2026 +0530

    fix(schema): boot validation reports a typo'd path whether or not the row has a default  [M4-05]

    settings/Schema.lua's S:Register counted a path as unresolved only when
    `S:ReadPath(p, row.path) == nil AND row.default == nil`. All nine rows in
    S.Schema declare a default, so the second conjunct was never true and the
    architecture-§5 boot check was structurally incapable of reporting anything —
    0 for a typo'd path exactly as for a correct one. tests/test_schema.lua's
    `assertEqual(S:Register(), 0)` was therefore asserting a constant.

    The two facts are independent. `row.default` is what the widget shows and what
    Defaults restores; resolving against NS.defaults.profile is what says the
    setting has somewhere to be written. A row with a good default and a typo'd
    path is the worst case, not the exempt one — the panel renders, the widget
    reads its default, the write lands on a key nothing reads, and nothing says
    so. Dropped the conjunct, so a path that does not resolve is reported full
    stop, and corrected the printed line, which said "missing default" for what is
    actually an unresolvable path. Session-only rows stay the one exemption: they
    route through their own get/set and have no db-backed home by design.

    Extended the existing case at tests/test_schema.lua:7-10 rather than adding
    one, so no case name moves. It now injects three probe rows and restores the
    schema after each: a typo'd path WITH a default and a typo'd path with none
    must each return 1 — two probes because the bug's whole shape was that only
    the second was reachable — and a session-only row with an unresolvable path
    must still return 0. A closing assertEqual(S:Register(), 0) pins that the
    probes are off again for every case below.

    Closes PM-R-02 (M4-05, PanelMaster half; spec C19+C24+C28, "Boot validation
    can fire").

    Verification: luacheck . 0 warnings / 0 errors in 25 files; lua5.1
    tests/run.lua 711 passed, 0 failed; lizard Warning cnt 0. Case count unchanged
    at 711 and `tests/run.lua --list` diffs clean against docs/test-cases.md
    (CR-normalized both sides). Red/green: restoring the `and row.default == nil`
    conjunct failed the case with "typo'd path, WITH a default was not reported
    (expected 1, got 0)" (710 passed, 1 failed); reverting returned 711/0.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

commit 5f482188be96aafdab63baf34c3517d778b299ec
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 14:56:20 2026 +0530

    refactor(bus): Canvas subscribes through NS.Schema.MSG_SETTINGS, not the literal  [M4-04]

    modules/Canvas.lua:743 registered for "Ka0s_PanelMaster_SettingsChanged" as a
    raw string while the two lines above it used NS.Registry.MSG_PANELS and
    NS.Registry.MSG_PANEL. settings/Schema.lua:21 has published S.MSG_SETTINGS all
    along and NS.Schema is the same table, so the constant was there to be used —
    a receiver quietly holding its own copy of a sender's name is exactly what
    architecture-§4's one-declaration rule is for.

    Switched to NS.Schema.MSG_SETTINGS and noted why the TOC order does not bear
    on it: settings/Schema.lua loads after modules/Canvas.lua, but Enable() runs
    at OnEnable, so all three names resolve at call time.

    Every `Ka0s_PanelMaster_*` occurrence left in core/, modules/ and settings/ is
    now either the single declaration site (modules/Registry.lua:19-20,
    settings/Schema.lua:20) or prose in a comment; no code path spells a message
    name twice. docs/ARCHITECTURE.md:298-300's bus catalog already names
    settings/Schema.lua as the sender and Canvas as the receiver, and still
    matches.

    Closes PM-R-11 (M4-04, PanelMaster half; spec architecture-§4).

    Verification: luacheck . 0 warnings / 0 errors in 25 files; lua5.1
    tests/run.lua 711 passed, 0 failed; lizard Warning cnt 0. Red/green: putting a
    literal back with one character dropped ("...SettingsChangd") failed 5 cases
    including 'Canvas: OnEnable subscribes the renderer to the bus' with "the
    renderer is not listening for Ka0s_PanelMaster_SettingsChanged" (706 passed, 5
    failed); reverting returned 711/0. That spread is the point — a literal that
    drifts from its sender silently deafens the renderer, and the constant makes
    the drift unrepresentable.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

commit 3fc4ec9f8788c71f7529e40c0ae01a2c3f687a84
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 14:54:44 2026 +0530

    docs(readme): ### Settings panel leads with the Tab | Covers table  [M4-08]

    documentation-§1 item 7 requires `### Settings panel` to open with a
    **Tab | Covers** table, one row per settings subcategory, and MAY follow it
    with per-panel prose. This README opened straight into a
    `| Setting | What it does |` table of the General page's nine rows, so the
    mandated per-tab overview was absent and a reader could not tell what the
    other tabs held without reading to the end of the section.

    Added the table with one row per registered subcategory — the landing page
    plus General, Panels and Profiles, exactly the four pages
    settings/Panel.lua:348-476 registers (SetBuildMain, then three
    O.RegisterOptionsPage calls). The existing Setting table stays as the
    permitted follow-on prose, now labelled as General's. The Profiles and Panels
    paragraphs lose the sentences the table now states and keep only what it does
    not (the shared Default profile, the immediate redraw, the name-then-Enter
    flow, the one-panel-at-a-time layout).

    The General table's nine rows are the nine schema rows at
    settings/Schema.lua:27-98, one for one.

    Closes PM-A-12 (M4-08, PanelMaster half; spec C5).

    Verification: luacheck . 0 warnings / 0 errors in 25 files; lua5.1
    tests/run.lua 711 passed, 0 failed; lizard Warning cnt 0. README-only, no
    case count moves, docs/test-cases.md untouched.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

commit 5d820eca4021d1b8a4bafc0d096357d74e661abe
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 14:52:17 2026 +0530

    docs(comments): the printer's upvalue list names files that exist, and the CRLF claim is true  [M4-10]

    Two stale comments, both cited in the plan, plus the count claim they carry.

    core/CoreSetup.lua's TOC-ordering note listed six files taking the printer as
    a `local print = NS.Print` file-scope upvalue and named modules/DebugLog.lua
    among them. That file has not existed since the LibKa0s-DebugLog-1.0 adoption
    (LIBKA0S-10) deleted it; its replacement, core/DebugLogSetup.lua, deliberately
    does NOT take an upvalue — it hands the descriptor a
    `function(line) NS.Print(line) end` closure precisely so it is immune to the
    AceConsole reclaim, which is why it carries no ordering constraint. The real
    list is five: modules/Unlock.lua, settings/Schema.lua, settings/Slash.lua,
    settings/PanelEditor.lua, settings/Panel.lua. Removed the phantom entry,
    corrected six -> five, and said why DebugLogSetup is absent so the omission
    does not read as a bug next time.

    The same "six" appeared verbatim in three more places, all restating this one
    claim: core/Util.lua's MOVED note, tests/test_libka0s.lua's Core-seam header,
    and docs/ARCHITECTURE.md's load-order bullet — which said "six" while listing
    exactly five. All corrected; leaving them would have left the finding half
    closed against a self-contradicting sentence.

    tests/test_vendor_sync.lua's normalization note said the working tree is CRLF
    "because `.gitattributes` pins `* text=auto eol=crlf`". No such pin exists —
    .gitattributes carries `*.sh text eol=lf` and nothing else, and the tree is in
    fact LF here. The CR strip is still correct, for the real reason: the blob is
    always LF while the working tree follows the local checkout, and git's
    core.autocrlf is on by default in a Windows install. Rewrote the paragraph to
    state that.

    Closes PM-R-09, PM-R-10 (M4-10, PanelMaster half; spec C3).

    Verification: every path now named in these comments resolves (ls
    core/DebugLogSetup.lua, tests/_kit/vendor_sync.lua; the five upvalue sites are
    the exact output of `grep -rn "local print = NS.Print" core modules settings`;
    README.md:378 is the provenance line the kit's default pattern matches).
    luacheck . 0 warnings / 0 errors in 25 files; lua5.1 tests/run.lua 711 passed,
    0 failed; lizard Warning cnt 0. Comment-only — no case count moves, so
    docs/test-cases.md is untouched.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

commit 1d207e24319a82b6161a7a9da52de0d22f92ee3d
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 14:49:31 2026 +0530

    refactor(core): the Core seam publishes exactly what the addon calls  [M4-07]

    core/CoreSetup.lua published NS.Format = printer.Format, the only occurrence
    of the name in the repo — no production caller, no test caller, and the
    degraded branch never answered it. That made it the one asymmetric member of
    a seam whose own comment states its member set is exactly what the addon
    calls, and it forced the Core parity case to carry a permanent `ignore` entry
    for a member written for nobody.

    Dropped the assignment and replaced it with a note saying why Format is not
    taken. tests/test_libka0s.lua's PUBLISHED list loses "Format" and the parity
    ignore list loses its Format clause, so the case now compares the full live
    surface against the full degraded surface with no carve-out but NS.Core
    itself.

    Closes PM-R-08 (M4-07, PanelMaster half; spec C7).

    Verification: luacheck . 0 warnings / 0 errors in 25 files; lua5.1
    tests/run.lua 711 passed, 0 failed (unchanged — no case added or removed);
    lizard Warning cnt 0. Red/green: re-adding NS.Format = printer.Format with
    "Format" back in PUBLISHED failed the Core parity case with 'Core stub: the
    degraded stub diverges from the live surface in 1 place(s) - Format is
    missing (live: function)' (710 passed, 1 failed); reverting returned 711/0.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

commit 856ced2b802835777467866cd7e0e9b4478bcd88
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 13:15:06 2026 +0530

    docs(perf): commit the whole-repo sweep — PanelMaster does NOT qualify for performance-§12  [M3-09 BLOCKED]

    M3-09 asked this repo to claim the performance-§12 no-combat-path exemption.
    The sweep the item itself requires as evidence disproves the claim, so the
    exemption is NOT claimed and no register row asserting it is written.

    Criterion (a) requires "no OnUpdate handler, no repeating ticker, and no event
    handler doing more than occasional work while the player is in combat".
    modules/Canvas.lua:565 installs a shared 10Hz OnUpdate driver — updateMouseover,
    one NS.Compat.MouseIsOver + SetAlpha per mouseover-tracked panel — the moment any
    panel has "Show on mouseover only" ticked (mouseover defaults to false at
    core/Constants.lua:306, and the driver is never destroyed once created). There is
    no InCombatLockdown gate and none is wanted: the fade is meant to keep working
    while the player is busy. Criterion (b) fails with it — a bucket around that loop
    would not read 0.000 by construction — and (c) does not apply, because this addon
    records nothing suspend could suppress.

    Neither 03_SPEC.md's C1 nor 04_EXECUTION_PLAN.md's M3-09 mentions this OnUpdate;
    PanelMaster is listed among the five exempt repos on the assumption that it has
    no combat path. The measurement disagrees.

    What this commit does deliver, all of it honest:

    - docs/performance.md — required unconditionally by documentation-§3 and absent
      until now (PM-A-05). It carries the committed sweep in full: 1 OnUpdate hit,
      4 RegisterEvent hits with the per-event work named, 0 C_Timer, 0 repeating
      timers; the per-hit "runs in combat?" column; and why (a), (b) and (c) each
      fail. Re-running the sweep is how anyone checks whether the answer changed.
    - The performance-§12 row added to ## Documented deviations in the previous
      commit (M3-08) is REMOVED. It asserted an exemption this evidence contradicts,
      and a false row in the register is worse than no row. The section now states
      explicitly why the Perf decline is deliberately not a row: it is reasoned only
      at pending/LEDGER.md ▸ PLAN-06, which per documentation-§3 means it is NOT
      ratified, so performance-§1 is a genuinely open MUST here and an audit is
      right to keep filing it.
    - CLAUDE.md says the same rather than pointing at a register row that no longer
      exists.

    NOT delivered, and blocked on an owner decision: the register row, and therefore
    PM-A-01…PM-A-06 and PM-A-09. Wiring performance-§1's harness and bracketing
    updateMouseover, or changing the rule, are both decisions above this milestone.
    The other three M3-08 rows (documentation-§1 item 6, toc-file-§1,
    documentation-§4) are unaffected and stand.

    Verification: luacheck 0/0 in 25 files; 711 passed / 0 failed / 0 skipped;
    lizard Warning cnt 0. Documentation-only — no code changed.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

commit 8a26740de6ee2c8b70dc77c2071d1b42e8268055
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 13:11:31 2026 +0530

    docs(arch): add the ## Documented deviations register and move this repo's records into it  [M3-08]

    documentation-§3's ninth mandated ARCHITECTURE.md section, with the exact row
    shape | Rule | What differs | Why | Decided | Re-check trigger |. Rule is a
    filename-§N reference rather than a paraphrase, because a paraphrase cannot be
    checked against the rule it claims to deviate from, and every row carries a
    trigger — a row without one is a permanent opt-out wearing a table's clothes.

    Four rows, all moved in from records that already existed somewhere else:

    - `performance-§12` — the Perf decline, from CLAUDE.md:36 ("Perf is declined"),
      reasoned at pending/LEDGER.md ▸ PLAN-06. M3-09 completes this row: it adds the
      committed whole-repo sweep, names which of §12's (b)/(c) applies, and writes
      docs/performance.md. The row is written here so there is one row rather than
      two for one decision.
    - `documentation-§1` (item 6) — the README ## Screenshots placeholder (PM-A-18),
      audit PM-014, carried from D-004, ledger PLAN-05 / ISS-01, GitHub #1.
    - `toc-file-§1` — no X-Curse-Project-ID and a four-badge README row (PM-A-19),
      audit PM-015, carried from D-001/D-002, ledger PLAN-03 / PLAN-04. A project id
      does not exist until the first upload, so the field cannot hold a true value.
    - `documentation-§4` — pending work lives in docs/pending/LEDGER.md rather than
      a root TODO.md (PM-A-20), audit PM-016. §4 forbids a TODO.md once released; it
      does not mandate one before.

    CLAUDE.md now points at the register rather than being a second home for the
    Perf decision — a ledger entry declining a rule with no register row is itself
    the deviation.

    Closes PM-A-18, PM-A-19, PM-A-20 (PanelMaster's half of M3-08).

    Verification: luacheck 0/0 in 25 files; 711 passed / 0 failed / 0 skipped;
    lizard Warning cnt 0. Documentation-only.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

commit 51f70428e4c61c70f13efaf4c1f556dcb58faf95
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 13:09:09 2026 +0530

    docs(refs): the last retired §N.M reference resolves by filename  [M3-07]

    The retired global section notation, swept across code, tests and config with
    libs/, tests/_kit/, docs/audits/, docs/reviews/ and docs/automated-tests/
    excluded — all three docs/ paths are frozen evidence.

    PanelMaster carried exactly ONE site, and it is code:

      settings/OptionsSetup.lua — "Ka0s standard §3.4:" on the onAceGUI seam

    §3.4 does not resolve to anything in the current standard. The rule it means is
    `options-ui-§1` (*The options library*), which is where the descriptor's
    optional-seam table lists `onAceGUI` and where the library's "resolve AceGUI
    through LibStub once at panel-build time" behaviour is written. The comment now
    cites that and says what the seam is for, rather than pointing at a number.

    Repo has no `standalone-windows-§N` or `packaging-§N` reference to convert to a
    bare filename, and no malformed or out-of-range reference: every one of the 40
    distinct `filename-§N` references in this repo range-checks clean against the
    current section counts (architecture 7, automated-tests 7, debug-logging 12,
    documentation 7, events-frames-taint 8, layout 3, library-stack 7,
    localization 5, options-ui 11, performance 12, savedvariables 5,
    slash-commands 6, testing 13).

    Acceptance: grep -rEn '§[0-9]+\.[0-9]' with all five exclusions returns 0.

    Comment-only edit; no behaviour changed.

    Verification: luacheck 0/0 in 25 files; 711 passed / 0 failed / 0 skipped;
    lizard Warning cnt 0.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

commit 88cdf31fbc2eedcfb5e000b7fc23cae2cbaa82e1
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 13:07:52 2026 +0530

    docs(gates): every gate statement names its checkpoint  [M3-06]

    Sweep 3f. "perf and complexity never fail a run" is true and, standing alone,
    misleading: it reads as "these two gate nothing", which automated-tests-§3's
    release gate contradicts. There are TWO checkpoints — the commit and the tag —
    and those two suites answer differently at each, so a single "Gates?" column
    could only ever describe one of them.

    - docs/testing.md: the Gates? table splits into "Gates the commit?" and "Gates
      the tag?"; the paragraph below it states the release gate (all four suites at
      pass plus zero functions above CCN 15, evaluated by /wow-addon:bump-version
      from the run's manifest.json, never by the runner, whose exit code is
      unchanged because the same script is the commit gate) and that a skip is NOT
      EVALUATED rather than passed. "Commits are gated on lint + tests only" becomes
      "The commit gate is lint + tests only".
    - docs/automated-tests/README.md § "What gates, and what only records": the same
      two-column split and the same release-gate paragraph, plus a note that
      suites.<name>.gates is descriptive and nothing reads it.
    - Both now record that this addon ships no tests/perf.lua, which is the one
      sanctioned skip that still passes the release gate and which
      automated-tests-§3 requires the release notes to state.

    ONE DEVIATION FROM THE ITEM AS WRITTEN, flagged rather than hidden. M3-06 says
    "the RESULTS.md lead-in comes from the re-vendored runner, not from this sweep."
    The v8 runner only writes that lead-in when RESULTS.md does not yet exist — it
    will not rewrite a file whose rows are the trend line (testkit version-8-docs.md,
    "The emitted RESULTS.md lead-in names the checkpoint"). So a fresh run in this
    repo would NOT have replaced the old paragraph, and M3-06's own acceptance grep
    could never have come back clean. The lead-in is therefore replaced by hand here,
    byte-for-byte with what run-automated-tests.sh:425-433 emits. Only the lead-in
    paragraph changed: no dated bundle was touched and no row in the trend table was
    altered or re-worded.

    Acceptance: grep -rn "never fail a run" docs CLAUDE.md, excluding the frozen
    docs/audits/, docs/reviews/ and dated docs/automated-tests/ bundles, returns
    three hits, all checkpoint-qualified (docs/testing.md:126,
    docs/automated-tests/RESULTS.md:10, docs/automated-tests/README.md:31).

    Closes PM-A-13 (PanelMaster's half of M3-06).

    Verification: luacheck 0/0 in 25 files; 711 passed / 0 failed / 0 skipped;
    lizard Warning cnt 0. Documentation-only — no code or test changed.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

commit 6f88fe7ac33a713783c1d028d88b93b64fa052b4
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 13:05:17 2026 +0530

    chore(tests): record the vendored runner as executable in the git index  [M3-05]

    tests/_kit/run-automated-tests.sh was mode 100644 in the index. Nothing could
    see it: this repo sets core.fileMode=false, so git never complains, and the tree
    is DrvFs, so `ls -l` reports rwxrwxrwx for a file the index calls
    non-executable. `ls -l` is not evidence here — `git ls-files -s` is.

    `git update-index --chmod=+x` (not `chmod +x`, which the index would ignore).
    The bit does not travel with `cp`, so it survives no re-vendor and is not in the
    file's bytes, which is why the byte-identity vendor-sync case cannot see it;
    testkit v8's tests/test_kitsync.lua is the library-side gate that makes
    forgetting loud rather than surviving three audit cycles.

    Verified: `git ls-files -s tests/_kit/run-automated-tests.sh` -> 100755;
    `tests/_kit/run-automated-tests.sh --help` runs with no `bash` prefix.

    Closes PM-A-11 (PanelMaster's half of M3-05).

    Verification: luacheck 0/0 in 25 files; 711 passed / 0 failed / 0 skipped;
    lizard Warning cnt 0.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

commit 80e2e2b49785d92cd7a4e3ee63282e783fde5a0c
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 13:04:39 2026 +0530

    test(degraded): assert every LibKa0s stub surface as a set, and /pm config answers every time  [M3-04]

    Four adopted seams — Core, DebugLog, Slash, Options — each carry an
    `if not lib then` branch whose member set is what a degraded install runs on.
    Each now has one Kit.assertSurfaceParity case (testkit v8), with the producing
    grep named in the case comment and every `ignore` entry justified beside it as
    data, so an intentional live-only member and a bug no longer read the same.

    The degraded arm of every case comes from the REAL loader fed a PARTIAL file
    list — loadPartial{ Slash = true } omits exactly libs/LibKa0s/Slash.lua and
    loads the rest — so the seam under test takes its stub branch while the addon
    runs on the real library everywhere else. Nothing is hand-stubbed. loadDegraded
    is now the empty end of the same mechanism rather than a second one.

    PM-R-07 fixed: settings/OptionsSetup.lua's stub latched its explanation behind a
    `said` flag, so the first /pm config on a degraded install said why the panel
    was unavailable and every one after it did nothing at all — which reads as a
    broken command, not a missing panel. The latch is gone. The once-per-session
    latches on the Core printer's notice and the console's stay: those lines RIDE
    OTHER OUTPUT, where repeating would drown the line the user asked for.
    /pm config's line rides nothing; it is the verb's whole answer. That rule is now
    written down in docs/ARCHITECTURE.md's seam table and smoke-tests.md step 13.

    M2-05's interim hand-written stub-member case ("Degraded install: the Slash stub
    answers the WHOLE member set, FormatKV included") is DELETED in this commit, as
    M3-04 specifies — the Slash parity case replaces it. Two things it carried that
    the primitive does not are kept inside the replacement: the reverse direction (a
    stub that grew a member the live path lacks) and the FormatKV byte-equality pin
    against lib.FormatKV, which is exactly the "right member set, wrong
    implementation" limit the primitive states in its own docs.

    Case count 707 -> 711: -1 (M2-05's case), +4 (one parity case per seam),
    +1 (the /pm config repetition case). docs/test-cases.md regenerated; the README
    badge corrected to 711 (it read 706, stale since M2-05).

    Planted violations, each shown red then reverted green:
      - Sl.FormatKV renamed out of the degraded branch -> Slash parity: "FormatKV is
        missing (live: function)", 710/1.
      - D:BufferSize removed from the DebugLog stub -> DebugLog parity, 710/1.
      - RefreshScalars removed from the Options stub -> Options parity, 710/1.
      - the `said` latch put back in OptionsSetup -> "/pm config went silent after
        the first invocation (expected 3, got 1)", 710/1.
      - the Core stub stops publishing NS.Util.print -> Core parity plus four
        existing degradation cases, 706/5.
      All reverted: 711 passed, 0 failed.

    Closes PM-R-07 (PanelMaster's half of M3-04); supersedes M2-05's interim case.

    Verification: luacheck 0/0 in 25 files; 711 passed / 0 failed / 0 skipped;
    lizard Warning cnt 0; --list diff against docs/test-cases.md empty after
    CR-normalizing both sides.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

commit ccbe6b2cee84b1659a4fbab883ee19c05c0f80f2
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 12:57:27 2026 +0530

    test(harness): derive the vendored library's load list from LibKa0s.xml  [M3-03]

    The runner and tests/test_libka0s.lua each carried a hand-typed copy of the
    vendored library's load list, and both copies were short: SIX of the eight
    scripts LibKa0s.xml pulls in. Perf.lua and PerfPanel.lua were never loaded by
    the headless suite. A short load list does not raise — it leaves the modules
    undefined for whichever cases never reach them — which is exactly how PM-A-10
    survived a year of green runs.

    Both copies are now Loader.xmlFiles("libs/LibKa0s/LibKa0s.xml") (testkit v8),
    which returns directory-prefixed paths in XML order (load order) and RAISES on
    an XML it cannot open rather than degrading to an empty list that reads clean.

    tests/test_harness.lua adopts the kit primitive alongside it:
      - the two hand-rolled directional suite-list cases collapse into one
        T.assertSuiteInventory("tests/", declaredSuites()) case, which reports every
        divergence in both directions in a single message. Kit.run also calls it
        automatically now that `dir` is passed explicitly, but that call dies before
        any case registers, so the named case is what docs/test-cases.md counts.
      - a new case pins that run.lua derives the library list from the XML, and
        compares the derived list against the XML entry-by-entry in order.

    docs/testing.md's harness description no longer says the runner hand-lists
    libs/LibKa0s/*.lua.

    Case-count delta, since M3-03 warned the count would move: it did NOT.
    707 -> 707. The library files loaded moved 6 -> 8, but no case in this repo was
    counted per library file; the only case that enumerated them (test_libka0s.lua's
    "the vendored library registered for real") is one case that now walks eight
    paths instead of six. Within the suite the arithmetic is -1 (two harness cases
    -> one) +1 (the new XML-derivation case) = 0. docs/test-cases.md regenerated.

    Planted violations, shown red then reverted green:
      - tests/test_planted_violation.lua, undeclared -> Kit.run dies with
        'exists but is not declared in the suites list'; with suiteInventory=false
        the named case reports the same, 706 passed / 1 failed.
      - run.lua reverted to the hand-typed six -> the XML-derivation case fails
        'hand-lists the vendored library's files again instead of reading its XML',
        706 passed / 1 failed.
      - OptionsScroll.lua removed from LibKa0s.xml with the file left on disk ->
        4 failed, including the vendor-sync gate and both Options-seam cases.
      All three reverted: 707 passed, 0 failed.

    Closes PM-A-10 (PanelMaster's half of M3-03).

    Verification: luacheck 0/0 in 25 files; 707 passed / 0 failed / 0 skipped;
    lizard Warning cnt 0; --list diff against docs/test-cases.md empty after
    CR-normalizing both sides.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

commit 6f19f335f97e9e57c1457e840e07b28a68d8b4d6
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 12:49:40 2026 +0530

    chore(vendor): re-vendor LibKa0s v1.8.0 and adopt the shared vendor-sync gate  [M3-01][M3-02]

    Re-vendors BOTH payloads from the LibKa0s v1.8.0 tag in one commit —
    `libs/LibKa0s/` and `tests/_kit/` — and bumps README.md:378's provenance
    line to v1.8.0. The gate reads that line as its input and compares both
    pairs against it, so a kit-only copy would point the gate at a tag whose
    `LibKa0s/` bytes this repo does not hold.

    Rewrites tests/test_vendor_sync.lua from ~140 hand-copied lines to a
    `VendorSync.register(_G.PM_TEST, {})` call into the newly vendored
    tests/_kit/vendor_sync.lua. Case names are unchanged, so
    docs/test-cases.md does not move (707 passed, unchanged). The kit's
    default README pattern and default pairs both match this repo, so no
    opts overrides are needed. The one `gsub("\r\n","\n")` on the
    working-tree side lives on inside the kit — the other side is a
    `git show` blob (LF) — with AbsorbTracker's header explaining why
    carried in.

    The absent-sibling path now reports SKIP with a reason instead of a bare
    `return` that registered as PASS.

    Closes: PM-R-05 (and the M3-01/M3-02 halves for this repo).

    Verification:
      diff -r ../LibKa0s/LibKa0s/ libs/LibKa0s/   -> empty
      diff -r ../LibKa0s/testkit/ tests/_kit/     -> empty
      lua5.1 tests/run.lua                        -> 707 passed, 0 failed, exit 0
      luacheck .                                  -> 0 warnings / 0 errors, 25 files
      lizard -l lua -x ./libs/* -x ./tests/_kit/* -> Warning cnt 0
      --list vs docs/test-cases.md (CR-normalized both sides) -> empty
      grep -c "if not tag then return end" tests/test_vendor_sync.lua -> 0
      Planted violation in libs/LibKa0s/Core.lua  -> FAIL case #1, exit 1; reverted -> green
      Planted violation in tests/_kit/loader.lua  -> FAIL case #2, exit 1; reverted -> green
      Skip path proven WITHOUT moving ../LibKa0s (other agents are using it):
        a scratch harness outside the repo registered the same gate with
        opts.sibling pointed at a non-existent path -> both cases SKIP with the
        reason naming the missing checkout, exit 0.
      Exec bit deliberately NOT set here (core.filemode=false; that is M3-05).

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

commit 12f9e1cabab8c78701fee5141cb9fc2b20157202
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 10:27:46 2026 +0530

    fix(slash): the degraded stub assigns FormatKV so the panel verbs survive  [M2-05]

    `settings/Slash.lua`'s `if not lib then` branch (opens :285, returns :316)
    re-implements the dispatcher so the host-owned PANEL verbs keep working, but
    never assigned `Sl.FormatKV` — assigned only on the live path at :381. Every
    panel verb that renders a `key = value` line calls it: `:101` (`/pm panel
    <name>`), `:124`/`:125` (`fitart`), `:181` (field read) and `:188` (field write
    echo). On an install with no `libs/LibKa0s/`, `/pm panel <name>` raised
    "settings/Slash.lua:101: attempt to call field 'FormatKV' (a nil value)" —
    reproduced before the fix, ok=true after.

    The stub reproduces the library's one line rather than routing to it, because
    on this path there is no library to route to; the case below pins it
    byte-for-byte against `lib.FormatKV` so the two cannot drift.

    Test: one hand-written case in `tests/test_libka0s.lua` asserts the degraded
    `NS.Slash` function-member set in BOTH directions (a missing member and an
    unlisted extra both redden) plus FormatKV's bytes. Deliberately NOT built on
    `Kit.assertSurfaceParity` — that primitive is M1-LK-05, adopted in M3-04, which
    replaces this case; M2 carries no M1 dependency.

    Closes: PM-R-01

    Verified: luacheck 0 warnings / 0 errors in 25 files; lua5.1 tests/run.lua
    707 passed, 0 failed (706 -> 707, the +1 being this case); lizard Warning cnt 0;
    `--list` byte-identical to the regenerated docs/test-cases.md. Red-then-green:
    (1) stub FormatKV removed -> "cannot answer FormatKV"; (2) an extra `Sl.Bogus`
    -> "grew unlisted members (Bogus)"; (3) lowercase colour escapes -> assertEqual
    byte mismatch. All three reverted, suite green.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

commit 1b5849b06f995d4b1cac7ba8a47dc1aa3a28a94a
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 10:18:57 2026 +0530

    docs: commit frozen 2026-08-05 review and standards-audit bundles

    These bundles are the evidence input to the consolidated remediation plan.
    They are frozen artifacts and are never rewritten.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01KQxehYAMgZtvAJDCJxXYRs

commit 3469f3ee642459d69d0a0dbce10b4a4827d989f9
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 01:44:09 2026 +0530

    docs: the README's FAQ, Troubleshooting and tail match the Ka0s collection

    AbsorbTracker, BankLedger and LootHistory all render FAQ and Troubleshooting as
    two-column tables; this repo was the only one still using bold-question prose.
    Converted both -- `Question | Answer` and `Problem | Fix` -- with the same
    content, tightened to fit a row, plus one new FAQ row for the question the Sunn
    section invites ("Do I need Sunn - Viewport Art installed?").

    Three more shape differences closed at the same time:

    1. A `Credits and libraries` section, which every other addon in the collection
       carries and this one did not. The bundled-library list and the artwork's
       CC BY-SA attribution both live there now.

    2. The LibKa0s provenance sentence moves off the top of the page into that
       section, phrased the way LootHistory phrases it. The vendor gate is
       unaffected -- test_vendor_sync.lua matches the sentence anywhere in the file
       and accepts the lowercase mid-sentence form, which is the case its own
       comment was written about. Note the wrapping is load-bearing: the pattern
       wants `bundles [LibKa0s](...)` adjacent, and a line break between them makes
       the gate fail.

    3. Version History becomes `Version | Date | Highlights` with <br>-separated
       highlights, replacing one wall-of-semicolons row. The date reads -- because
       0.1.0 has not shipped; it gets filled in at release.

    Green gate: 706/706 tests, luacheck 0/0.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01XhE1LqKvBP1TZDkkAnni9b

commit 26e6067b459fe475b65396bf69ccadd574f9ff57
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 01:10:49 2026 +0530

    docs: README stops duplicating the artwork pipeline; the deviation note moves with it

    sync-docs output that was left uncommitted by the finalize pass.

    README.md is the player-facing document (documentation-§1). Its 'Contributing
    artwork' and 'The artwork pipeline' sections restated what
    docs/artwork-spec.md already covers across seventeen sections -- quick start,
    the folder tree, naming rules, what the cleaner does, the tool reference -- so
    the README carried a second copy that could drift from the first. They are
    replaced by one player-facing 'Using your own artwork' section, and the README
    still points at docs/artwork-spec.md for the rest.

    One paragraph was NOT duplication: the note recording that tools/artwork/ is an
    accepted, documented deviation from the standard, which defines no location for
    build tooling. That moves into docs/artwork-spec.md rather than being dropped,
    with its acceptance date and the related ARTWORK-04/05/06 ledger entries.

    Documentation only. 706 tests pass, luacheck 0/0, zero functions over CCN 15.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01EhKAb8o8K8KUjpHArvE1ky

commit 6f7990bc0cabfd3e68cbf3532ff0f0e978f09c72
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 00:51:27 2026 +0530

    automated-tests: put the recorded Max CCN back, and correct PM-004's ownership

    Two corrections to yesterday's doc pass, both of the same family: a document
    saying something the evidence beside it does not.

    1. Commit 21cffd8 edited a GENERATED table row in place, changing run
       20260804-215132's Max CCN from the recorded 0 to '15 dagger'. That is the one
       thing performance-§10 forbids: a table edited to read what it should have
       measured is indistinguishable from one that measured it, and the row then
       disagreed with the manifest.json it links, which still records 0. The 0 is
       restored. The trend column reads 51 -> 0 -> 15 again and the prose beside it
       explains that the middle figure is an instrument fault -- the annotation does
       the work the edit was doing, without falsifying the record.

    2. ANALYSIS.md claimed 'no deviation ID or review finding owns' the missing
       tests/perf.lua. PM-004 owns it exactly, against performance-§9, naming the
       same gap and the same remedy. Corrected to cite it.

    The bundle's measured artifacts -- manifest.json, complexity.txt, lint.txt,
    tests.txt -- are untouched. Only the written analysis, added hours ago in this
    same session and never yet read, is corrected.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01EhKAb8o8K8KUjpHArvE1ky

commit b25c34d2563cb39f1d06ad79634edc47c7bd98cb
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 00:32:58 2026 +0530

    docs: write the newest bundle's ANALYSIS.md and re-anchor the RESULTS.md prose

    The 20260804-233329 bundle shipped without an ANALYSIS.md — the runner does not
    write one and Step 2 of the automated-tests playbook was skipped when the bundle
    was regenerated, which left the newest run, the one RESULTS.md points at as
    "Current state", with no write-up at all. Written now against the playbook's
    template: every figure cited to that bundle's own manifest.json or a suite
    artifact in it, every suite row linking its evidence, complexity reported in full
    with totals AND averages, and perf recorded as the skip it is rather than
    softened into a pass.

    The run's headline is that it closes the CCN work: zero functions over CCN 15
    over 1348 functions, with the ceiling at exactly 15 in two named functions. The
    previous run had the same ceiling and an instrument that could not report it, so
    the analysis says that plainly rather than letting 51 -> 0 -> 15 read as
    complexity vanishing and returning.

    RESULTS.md standing sections re-anchored to the newest run:

    - Test suite: 706 was "up ten on the previous run" when 20260804-215132 was
      newest; it is flat there now and up ten on the baseline.
    - Complexity watch list: the prose said "the previous run warned on nine
      functions", which was true of the baseline, not of the previous run. Corrected,
      and all nine are named with their CCNs rather than counted — read off
      20260804-182223/complexity.txt.
    - A plain paragraph naming which run stamps carry a Max CCN of 0, why (the kit
      read CCN_MAX from lizard's warnings block, empty at zero warnings), and where
      the true figure is (that same bundle's complexity.txt; 15 for 20260804-215132).
      The frozen rows keep their numbers — a hand-edited record is worse than a wrong
      one because it reads as measured.

    Two more counts named rather than stated:

    - docs/ARCHITECTURE.md: NS.LIBKA0S_MISSING's "three other files" are now named —
      core/DebugLogSetup.lua, settings/OptionsSetup.lua, settings/Slash.lua.
    - docs/superpowers/plans/2026-08-04-ccn-elimination.md: step 8 specifies the
      fifteen-positional-argument buildSectionQuads, which 645868a replaced with one
      named bar table (PARAM 3 in the newest bundle). The plan text stands as the
      reasoning at the time; a dated outcome note carries the correction.

    No version bump, no CHANGELOG entry, no tag. Gate: luacheck 0/0 in 25 files,
    706/706 tests, lizard zero functions over CCN 15.

commit 21cffd894b769d4b91a434dc97ae4dad96540a5b
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 00:12:38 2026 +0530

    docs: correct the complexity trend line and the watch list's stale numbers

    sync-docs over the live doc set after the CCN branch. The generated inventory
    docs/test-cases.md already matched `lua tests/run.lua --list` byte for byte, the
    README's 706/706 badge agrees with it, and DEPENDENCIES.md's file:line citations
    all still resolve — so the drift was confined to the automated-test record.

    RESULTS.md
    - The 20260804-215132 row reported Max CCN 0. Its own frozen complexity.txt maxes
      at 15; the old kit read the figure out of lizard's warnings block, which is
      empty at zero warnings, so the field had no input rather than a value. Left
      uncorrected the trend read 51 -> 0 -> 15, i.e. complexity vanishing and coming
      back. The bundle keeps its wrong number (bundles are frozen); the table is
      corrected with a footnote naming the cause and the kit fix.
    - The complexity watch list still said "as of 20260804-215132" while the newest
      run in the table is 20260804-233329.
    - modules/Artwork.lua's band row read 1166 LOC and "334 lines off the 1500 band".
      It is 1188 and 312, up 101 across this branch from 1087.

    ARCHITECTURE.md
    - Registry.FitToArtwork -> Registry:FitToArtwork; it is a method, and every other
      method in that document is written with the colon.

    No version bump, no CHANGELOG entry. docs/performance.md and docs/perf-runs/ are
    still absent and are left absent rather than invented.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01EhKAb8o8K8KUjpHArvE1ky

commit 21cffd894b769d4b91a434dc97ae4dad96540a5b
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 00:12:38 2026 +0530

    docs: correct the complexity trend line and the watch list's stale numbers

    sync-docs over the live doc set after the CCN branch. The generated inventory
    docs/test-cases.md already matched `lua tests/run.lua --list` byte for byte, the
    README's 706/706 badge agrees with it, and DEPENDENCIES.md's file:line citations
    all still resolve — so the drift was confined to the automated-test record.

    RESULTS.md
    - The 20260804-215132 row reported Max CCN 0. Its own frozen complexity.txt maxes
      at 15; the old kit read the figure out of lizard's warnings block, which is
      empty at zero warnings, so the field had no input rather than a value. Left
      uncorrected the trend read 51 -> 0 -> 15, i.e. complexity vanishing and coming
      back. The bundle keeps its wrong number (bundles are frozen); the table is
      corrected with a footnote naming the cause and the kit fix.
    - The complexity watch list still said "as of 20260804-215132" while the newest
      run in the table is 20260804-233329.
    - modules/Artwork.lua's band row read 1166 LOC and "334 lines off the 1500 band".
      It is 1188 and 312, up 101 across this branch from 1087.

    ARCHITECTURE.md
    - Registry.FitToArtwork -> Registry:FitToArtwork; it is a method, and every other
      method in that document is written with the colon.

    No version bump, no CHANGELOG entry. docs/performance.md and docs/perf-runs/ are
    still absent and are left absent rather than invented.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01EhKAb8o8K8KUjpHArvE1ky

commit 55e9d72a2b3fe98d6e62d90259377dd618a62656
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Aug 4 23:34:26 2026 +0530

    re-vendor LibKa0s v1.7.0 (testkit rev 6)

    Whole-folder copy of libs/LibKa0s and tests/_kit from the v1.7.0 tag, with the
    README provenance line moved in the same commit (library-stack-§7).

    testkit rev 6 fixes two silent runner bugs that both showed up here: Max CCN
    reported 0 for any addon at zero warnings, and RESULTS.md rows never appended
    at all in a CRLF repo. The bundle in this commit is the first run produced by
    the fixed kit, so its Max CCN is the real repo ceiling and its row actually
    reached the trend line.

    Earlier bundles keep the figures they were written with -- they are frozen
    records (automated-tests-§1) and the true value was always in their own
    complexity.txt.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01EhKAb8o8K8KUjpHArvE1ky

commit 645868a3f0cf91cdd448fd313a594a3970018b0a
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Aug 4 23:09:06 2026 +0530

    buildSectionQuads: one named bar table, not thirteen positional arguments

    The signature took 15 positional arguments to avoid a per-call hash table, on
    the measurement that the table costs +616 bytes/call and +10.8% on the
    composite path. The measurement is real; the conclusion did not hold, because
    this is not a per-frame path.

    BuildArtSpec is reached only through Canvas.BuildSpec <- Canvas:Render(id),
    which runs on a config change, a panel drag, a profile switch or load. The
    module's one OnUpdate is the 10Hz mouseover driver, and it only calls SetAlpha
    on a cached f.__spec -- it never reaches Render. So the allocation lands a few
    dozen times when someone moves a panel, not sixty times a second, and 616 bytes
    at user-action frequency buys nothing worth a 15-argument boundary.

    Params 15 -> 3. CCN unchanged at 12; lizard's parameter threshold is 100, so
    this never affected the gate in either direction. Behavior unchanged: the
    twelve composite cases cover every field moved into the table -- crop, flip,
    rotation, tile, section order -- and all 706 pass.

    The comment records that the tests were mutation-checked rather than assumed:
    swapping flipH/flipV in the destructuring fails one case, fv0/fv1 fails three.
    So this is a readability change the suite already stood behind, not a fix for a
    silent failure -- and the comment says so rather than overselling it.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01EhKAb8o8K8KUjpHArvE1ky

commit f75ab41e587652579334b90bb1f47acae65450d8
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Aug 4 21:57:24 2026 +0530

    ccn: pin the orphan tally, drop a dead seam, make the record true

    Closes feat/fix-ccn. Two code fixes and the generated artifacts.

    The DebugLog frame-count regression test did not pin what it names. Mutation
    check: deleting the orphan branch in addFrames entirely left the suite at
    706/706 green, because the fixture had no orphan and "0 orphaned" passes just
    as happily against code that never counts one. It now makes a real orphan —
    the record lifted straight out of the array Registry owns, leaving Canvas
    holding its frame — and asserts 1. Both mutations (branch removed, branch
    inverted) now fail the suite.

    core/DebugLogSetup.lua's activeFrames() was a dead seam the CCN split left
    behind, and its comment stated a reason that was no longer true: "asked for
    twice below — once to find orphans, once to count". master read the map twice;
    addFrames does both in one pass, so the wrapper had one caller and its stated
    justification had evaporated. Inlined. The header comment above it said "the
    four writers below" when only three of the four were writers; it now names
    addHeader, addPanel and addFrames.

    Artifacts brought into line with the branch:

    - README test badge 696 -> 706. docs/test-cases.md was already current at 706
      and regenerating it produced a byte-identical file (CRLF preserved).
    - A fresh automated-test bundle, docs/automated-tests/20260804-215132/, with
      its ANALYSIS.md, and the RESULTS.md row prepended by the runner. Not a
      release run.
    - RESULTS.md's watch list rewritten against it. The functions table now reads
      "None." — nine warnings to zero, no threshold moved and nothing suppressed.
      The old dispositions are not carried forward; every function they described
      is gone. The FILES table keeps its real entries against the new numbers, and
      modules/Artwork.lua is noted at 1166 LOC (was 1087) — the split that fixed
      the CCN is what grew it.

    Deliberate, disclosed, and left standing: buildSectionQuads' 15 positional
    arguments (modules/Artwork.lua). The ctx table was measured at +616 bytes/call
    on the composite render path and reverted; the transposition risk is real,
    lizard cannot catch it, and it is recorded in the bundle's ANALYSIS.md rather
    than churned.

    No behavior change, no version bump, no CHANGELOG entry.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01EhKAb8o8K8KUjpHArvE1ky

commit ee1ad6a6c2f04cae2938b99db7928f92b58f936e
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Aug 4 21:12:26 2026 +0530

    ccn: drop the ctx bag from buildSectionQuads, keep GetScale's `or`

    Two review findings against the CCN work, both repaired by going back to what
    master did rather than by moving the assertion.

    buildSectionQuads took a 15-field ctx table assembled at the call site, which
    put one extra hash table on every composed BuildArtSpec call. Measured over
    100k iterations with the collector stopped, same fixture throughout:

      composite FILL/FIT bytes/call   master 5680  pre-fix 6296  now 5592
      composite TILE     bytes/call   master 4368  pre-fix 4984  now 4280
      single texture     bytes/call   master 2496  pre-fix 2408  now 2408

    The arguments are positional now. Fifteen of them is the right price for a
    helper on a render path; a shorter signature was the only thing the bag bought.
    CCN is unchanged, so this costs nothing at the gate.

    wow_mock's GetScale had turned `f.__scale or 1` into an `== nil` test via the
    shared reader(). The two differ only for a stored false, which nothing in the
    addon can produce, but `or` is what every existing assertion was written
    against, so GetScale goes back to being spelled out and reader() loses its
    default argument (no other caller passed one, and with no default the two
    spellings are identical).

    Both the reader comment and the test comment claimed `or` would swallow a
    recorded 0. It would not — 0 is truthy in Lua, so `(0 or 1)` is 0, and only nil
    and false ever reach an `or` default. Corrected in both places, and the test now
    pins all three cases: unset reads 1, a recorded 0 reads 0, a stored false reads
    the default.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01EhKAb8o8K8KUjpHArvE1ky

commit f481a878916a4c3d8a94e23b0b294405cae9fcad
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Aug 4 19:41:03 2026 +0530

    ccn: Sanitize 40->9, stub __index 33->4, R:Set 29->6, S.Themes 22->1, CliPanel 17->12

    Chunk 2 of the CCN elimination pass. Behavior unchanged throughout; the rejection
    strings, the chat output, the SavedVariables shape and the sort order are all
    byte-identical.

    R.Sanitize (modules/Registry.lua) 40 -> 9. The flat list of ~45 defaulting
    statements becomes five module-level rule tables (CLAMPED, FREE_NUMBERS,
    NONEMPTY_STRINGS, POINT_FIELDS, ENUM_FIELDS, BOOL_FIELDS) applied by three
    helpers — sanitizeNumbers, sanitizeTokens, sanitizeFlags. The carve-outs stay
    written out: enabled is ~= false, an empty accentEdges is not repopulated, an
    empty artCustomPath survives, strata keeps its own predicate. Every per-field
    comment that carried a decision moved onto its table row.

    R:Set (modules/Registry.lua) 29 -> 6. The nine-arm elseif chain on `kind`
    becomes a module-level COERCE table, one function per kind, built once at load.
    A kind absent from the table is stored verbatim, which is what the old
    fall-through did for "string". Precedence and every rejection sentence are
    unchanged.

    stubFrame's __index (tests/wow_mock.lua) 33 -> 4. The thirty-arm chain becomes a
    module-level METHOD table of factories built once at load, with reader/writer/
    boolWriter/rgbaWriter collapsing the two dozen trivial recorders. reader keeps
    the explicit nil test so a recorded 0 or false survives. Eight characterization
    cases pinning the stub were added to tests/test_harness.lua FIRST and passed
    against the unrefactored stub.

    S.Themes (modules/SunnArt.lua) 22 -> 1. Split into clampSections (which removes
    a real duplication — the same three-branch clamp was written twice in two
    spellings), isRegisterable, collectRegistered, collectKnownPacks, and the
    comparator hoisted to module level.

    Sl:CliPanel (settings/Slash.lua) 17 -> 12. doDeleteAll, doFitArt and
    parseFieldValue extracted; what is left is pure routing, every branch a distinct
    user-facing outcome.

    docs/test-cases.md regenerated.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01EhKAb8o8K8KUjpHArvE1ky

commit a3fa02ab161ed7e0893cc77a7d2a1e26aa9536a1
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Aug 4 19:30:01 2026 +0530

    ccn: BuildArtSpec 51->13, BuildSpec 24->6, release 17->6, Diagnose 21->2

    Chunk 1 of the CCN elimination: every function over lizard's 15 in
    core/DebugLogSetup.lua, modules/Artwork.lua and modules/Canvas.lua, brought
    under the bar with no behavior change.

    Artwork.BuildArtSpec 51 -> 13. The five-arm fill matrix becomes a MODULE-LEVEL
    FILL dispatch table built once at load, keyed by fill name with TILE as the
    fall-through arm the old `else -- TILE` was. Eight more file-local helpers own
    one job each: nativeSize (also killing the duplicate block in NativeSize),
    presentedExtent, toTextureAxes, artPlacement, artTint, artFlips,
    artRenderHints, contentCropV0, clampTiledComposite, singleTextureQuad and
    buildSectionQuads.

    Canvas.BuildSpec 24 -> 6, split into addGeometry / addAppearance /
    buildAccentSpec. The artwork call now reads spec.width/spec.height, so the
    "fit art to the CLAMPED size" rule is structural rather than a convention.

    Canvas release 17 -> 6, via clearBackdrop (the idiom the panel frame and each
    accent bar both used), releaseAccents, releaseArt and poolName.

    D:Diagnose 21 -> 2, via activeFrames / addHeader / addPanel / addFrames, all
    hoisted to file scope so they are built once rather than per attachDiagnose
    call, and all still resolving through NS at CALL time — the property that lets
    this file sit in core/. The unreadable counting IIFE is gone.

    Two characterization tests written and seen passing against the unrefactored
    code first, both covering behavior the plan flagged as unpinned: a released
    frame is inert (accent bars hidden, backdrops and art textures cleared), and
    Diagnose's active/pooled/orphaned line.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01EhKAb8o8K8KUjpHArvE1ky

commit c56197be428f7c7fa10a18a82f76222f82a11b8b
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Aug 4 19:15:57 2026 +0530

    docs: CCN elimination plan

    Per-function refactor plan for every lizard CCN>15 warning in this addon.
    Design: LibKa0s/docs/superpowers/specs/2026-08-04-ccn-elimination-design.md

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01EhKAb8o8K8KUjpHArvE1ky

commit 63b7aa10e02c4694e03bc7151d1be60088b4db3b
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Aug 4 18:34:36 2026 +0530

    automated-tests: the watch list is two real tables, with a Band column

    Two rendering faults in RESULTS.md and ANALYSIS.md.

    THE FUNCTION ROWS HAD NO HEADER. They were markdown table rows with no header
    line and no separator, so they rendered as literal pipes rather than a table.
    Now: Function | CCN | Location | Disposition, with the header and rule.

    THE FILE BAND WAS A RUN-ON SENTENCE. "Files in the 1000-1500 band: a.lua (1256)
    -- accepted...; b.lua (1306) -- already tracked as BL-24." Two files were
    readable; four (KickCD) were not, and there was nowhere to put a second band.

    It is now a table with the band as a COLUMN, not a heading:

      | Band | File | LOC | Disposition |
      | 1000-1500 (on notice) | tests/test_x.lua | 1256 | accepted -- case count |
      | > 1500 (over cap)     | data/Gen.lua     | 2893 | already tracked as ... |

    A column rather than a heading per band because the bands are open-ended: two
    exist today, the standard may add or move one, and a heading-per-band makes
    every addon's record need restructuring when that happens. It also keeps a file
    that MOVED between bands on one line in the diff instead of two.

    Both tables now say "None." / "None -- no file reaches 1000 LOC" when empty
    rather than being dropped, so an empty watch list reads as a result.

    Dispositions are unchanged in substance -- they are the same ones recorded with
    their evidence earlier today, re-rendered. Where a summary sentence stood in for
    a long tail (BankLedger's fifteen, KickCD's twenty), every entry is now its own
    row with its own disposition rather than being folded into prose.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01EhKAb8o8K8KUjpHArvE1ky

commit f0283bfe1a13bdb4fef0bd38f237f9662bce1ef3
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Aug 4 18:24:41 2026 +0530

    automated-tests: RESULTS.md gains size + averages, and prose for all four suites

    Two gaps in the trend record.

    THE TABLE WAS COMPLEXITY-THIN AND SIZE-BLIND. It carried CCN warn and Max CCN
    but no average and no size, so a row could not be read against the next one
    across a change in the addon. Columns are now Run, Version, Lint w/e, Files,
    Tests, Perf, NLOC, Funcs, Avg NLOC, Avg CCN, Max CCN, CCN warn, Verdict. An
    average without its total, or a total without its average, cannot be compared.

    THE ONLY PROSE WAS ABOUT COMPLEXITY. The watch list came first and it was about
    to become the only narrative the record ever carried, which teaches a reader
    that the other three suites are pass/fail lights. They are not, and each now has
    a standing section:

      Test suite -- case count and what it covers, so a count that stops moving
                    while the addon grows is visible; the table cannot show that.
      Lint       -- clean over how many files, AND what the config excludes. A 0/0
                    row means nothing without knowing what was in scope.
      Perf       -- the scenarios and what they pin, or a plain statement that the
                    addon ships none: the record is then silent about runtime cost
                    and performance-§9's zero-overhead evidence does not exist for
                    it. A permanent skip is a standing fact, not a tooling gap.

    tests/_kit re-vendored to LibKa0s v1.6.3 (kit revision 5) and the README
    provenance moved with it. That revision also stops two ways the table could
    mislead: a suite that was not SELECTED now renders as an em dash rather than its
    zeroed counters (a --suite lint run used to write "0/0" into Tests, which reads
    as a full run finding no tests), and a changed column set no longer silently
    recreates the file and drops every previous row.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01EhKAb8o8K8KUjpHArvE1ky

commit 559e06dcdfaf02cc95ea48eb17b090239efb0dbd
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Aug 4 12:28:45 2026 +0000

    automated-tests: ANALYSIS.md links every artifact and reports complexity in full

    Two gaps in the write-up, both in the Suites table.

    The perf row named its scenario count but linked nothing, so the one suite whose
    output is a JSON record was also the one you could not click through to. Every
    row now links its artifact -- lint.txt, tests.txt + test-cases.md, perf.txt +
    perf.json, complexity.txt -- and a skipped suite links nothing and says so,
    because there is no artifact to link.

    The complexity row reported four numbers and dropped four. lizard's footer is

      Total nloc   Avg.NLOC  AvgCCN  Avg.token   Fun Cnt  Warning cnt   Fun Rt   nloc Rt
            7532       6.5     1.7       45.9     1047            2      0.00    0.02

    and the manifest was keeping only the totals plus AvgCCN. The averages are what
    make one run comparable to the next ACROSS A CHANGE IN SIZE: a total that rose
    because the addon grew is a different fact from an average that rose because it
    got denser, and only the second is a complexity signal. Reporting totals alone
    makes a growing addon look like a degrading one every release, until nobody
    reads the row. All eight fields are now recorded and reported, in their own
    table, alongside the two derived file counts.

    tests/_kit re-vendored to LibKa0s v1.6.2 (kit revision 4, which is what records
    the extra fields) and the README provenance line moved with it.

    The bundle from earlier today was removed rather than kept beside this one: it
    was produced minutes ago by a superseded kit revision and its analysis cannot
    carry the metrics, so keeping it would seed the trend line with a row that can
    never be compared against its successors. Retention applies to genuine runs over
    time, not to churn from getting the tooling right within one hour.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01EhKAb8o8K8KUjpHArvE1ky

commit 45521f2dc6cd3742c1f9a333ef5f814ff45a6443
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Aug 4 11:50:57 2026 +0000

    automated-tests: kit rev 3 — ANSI-clean artifacts, local-time YYYYMMDD-HHMMSS, ANALYSIS.md

    Three fixes to the record produced during adoption, all found by looking at the
    artifacts rather than by a test.

    lint.txt and tests.txt held raw ANSI escapes. luacheck and the harness colour
    their output when they think a terminal is attached, so the stored evidence read

      Checking core/Compat.lua    <0x1b>[0m<0x1b>[32m<0x1b>[1mOK<0x1b>[0m

    -- unreadable in an editor and pure noise in a diff between two runs, which is
    most of what a stored artifact is for. Fixed upstream in LibKa0s v1.6.1.

    Run directories are now stamped in LOCAL time as YYYYMMDD-HHMMSS, not UTC as
    YYYY-MM-DD-HHMMSS. A record is read by the person who ran it, usually minutes
    later; a folder name that disagrees with their clock costs a mental conversion
    every glance. manifest.json's startedAt carries an explicit UTC offset rather
    than a Z, so the instant stays unambiguous once the record outlives the machine.

    ANALYSIS.md was missing. The runner writes data and a reader writes the
    analysis, which is the design -- but automated-tests-§5 makes it a SHOULD for
    any run, and shipping eight bundles with none was a gap rather than a choice.
    Each bundle now carries one to the playbook's uniform template, and RESULTS.md
    carries the complexity watch list the section requires below its table.

    tests/_kit re-vendored to LibKa0s v1.6.1 (kit revision 3) and the README
    provenance line moved with it. The bundles were regenerated rather than renamed:
    a folder name, the manifest's `run` field and its startedAt all have to agree,
    and rewriting two of them by hand to match a rename is exactly the hand-edited
    record the section forbids.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01EhKAb8o8K8KUjpHArvE1ky

commit 8efb499c9db698017c02060a9d0409a18858551f
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Aug 4 10:27:09 2026 +0000

    Adopt automated-tests: consolidated test records, retire docs/complexity.md

    Standard v2.19.0's `automated-tests` section. All four out-of-game suites now go
    through one vendored runner and every run is recorded.

      tests/_kit/run-automated-tests.sh          -- from LibKa0s v1.6.0 (kit rev 2)
      docs/automated-tests/README.md             -- what it is, how to run it
      docs/automated-tests/RESULTS.md            -- one row per run, ALL four suites
      docs/automated-tests/<stamp>/              -- first frozen bundle

    lint and tests GATE; perf and complexity are RECORDED, NEVER GATING. That is a
    deliberate refusal to do the obvious thing now that all four sit side by side:
    a threshold that fails a run teaches everyone to reach for --no-verify, after
    which the gate protects nothing and the habit remains. They yield `amber`.
    A missing tool is a SKIP RECORDED WITH ITS REASON, never a pass -- a green run
    that silently measured nothing is worse than a red one, because it is believed.

    docs/complexity.md is RETIRED. Its raw output is each bundle's complexity.txt
    and its trend line is RESULTS.md, which stays ONE file overwritten in place --
    performance-§10 warned that a dated pile "would scatter across files the one
    comparison the report exists to make", and that is exactly why the trend line
    did not move into the bundles with the evidence.

    docs/perf-runs/ narrows to IN-GAME captures, where the addon has one: a script
    cannot produce those, so they keep their standing cumulative store. Offline runs
    belong to the bundle for the run that produced them.

    .gitattributes gains `*.sh text eol=lf`. Everything else here is CRLF, and a
    shebang followed by CRLF makes the kernel look for an interpreter literally
    named "bash\r". Without this the vendored runner is broken on EVERY checkout.

    tests/_kit re-vendored whole-folder from LibKa0s v1.6.0 and the README
    provenance line moved with it, so test_vendor_sync stays green -- the gate
    compares the file SET first, and a consumer still holding four files fails there
    before it reaches a content diff.

    Docs synced: docs/testing.md's complexity section replaced with the consolidated
    runner and its gating table; CLAUDE.md, DEPENDENCIES.md, ARCHITECTURE.md,
    file-index.md and common-tasks.md updated wherever they named complexity.md.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01EhKAb8o8K8KUjpHArvE1ky

commit 3142d25524852083006d5a8846ff4c4e1a70c173
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Aug 4 07:38:09 2026 +0000

    docs: sync the doc set to the code — Desaturate, Blend and the current catalog

    Documentation drift, all of it in one direction: the docs still describe the
    addon as it was before `feat(artwork): tint every piece, add Desaturate and
    Blend mode` and before the catalog was rebuilt from the folder tree. No code
    changes.

    Desaturate and Blend mode were shipped and documented nowhere. Both are real
    editor controls (settings/PanelEditor.lua:853, :858) and real record fields
    (C.PANEL_TEMPLATE), and every doc that enumerates the artwork surface omitted
    them: the README's per-panel table, its "What's new" bullet, ARCHITECTURE's
    panel record and artwork-field table. Worse, two docs asserted the OPPOSITE
    of what ships — the README said full-color art "is left alone by the tint"
    when every piece takes it now, and smoke test 5e-3.6 told a tester to expect
    that "there is no blend-mode control", which is a smoke test that FAILS on
    correct code. 5e-4 was checking for a tintable split that no longer exists
    (the color pair is unconditional on purpose, so it stops shoving the rows
    below it up and down as you page the dropdown).

    Counts and citations, each re-derived rather than copied forward:

    - "Fifteen art* fields" -> sixteen. Sanitize snaps four of the closed lists,
      not "the four enum values": artBlend is deliberately NOT snapped there, it
      is defended in BuildArtSpec instead, and that asymmetry is now stated.
    - "The five closed lists" -> six, with which kind validates each one.
    - Bundled art is 1024x1024, not 512x512 — 101 files, all of them, verified
      against the TGA headers and against the catalog's declared w/h.
    - The Sunn manifest measures 250 of 270 section files at 512x256, not of 262.
    - Three DEPENDENCIES.md evidence citations had drifted by a line or two
      (test_vendor_sync.lua:88 -> :89 and :73-76 -> :74-76, make_poster.py
      :386-388 -> :387-389). An evidence doc whose file:line is off is the one
      kind of wrong that reads as right.

    Stale names, which are the ones a user actually trips over: the README's CLI
    example and smoke test 5e.2 both named `runic-sigil` / `General: Runic Sigil
    (B&W)`, an id and a category that stopped existing when the pipeline started
    deriving both from the folder tree. Now `class-death-knight` /
    `Class: Death Knight`. And Sunn rows are listed under `Sunn -> Art Pack 2` —
    the category separator is ASCII, not the "▸" the docs were showing, so
    anything hunting for that glyph in the dropdown will not find it.

    Per-section Sunn rows were dropped when the composite adapter landed, but
    ARCHITECTURE's module map and the README's feature bullet still advertised
    "one section at a time". Both now say one entry per theme, which is what the
    adapter synthesizes and what the rest of both documents already said.

    docs/complexity.md is untouched — it is generated, and regenerating it is a
    release checkpoint (performance-§10), not a doc-sync step. The frozen
    audit/review bundles under docs/ are untouched for the same reason they are
    frozen.

    Gates after the change: luacheck 0 warnings / 0 errors in 25 files;
    lua tests/run.lua 696 passed, 0 failed, 696 total.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01EhKAb8o8K8KUjpHArvE1ky

commit 453565efdd28c317e8a5cfab55f4dbab17703963
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Aug 4 05:52:01 2026 +0000

    docs: commit the 2026-08-03 review and 2026-08-04 standards-audit bundles

    Frozen dated bundles as produced by /wow-addon:review and
    /wow-addon:standards-audit. Not edited after the fact.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01EhKAb8o8K8KUjpHArvE1ky

commit 453565efdd28c317e8a5cfab55f4dbab17703963
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Aug 4 05:52:01 2026 +0000

    docs: commit the 2026-08-03 review and 2026-08-04 standards-audit bundles

    Frozen dated bundles as produced by /wow-addon:review and
    /wow-addon:standards-audit. Not edited after the fact.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01EhKAb8o8K8KUjpHArvE1ky

commit 7321a80fb7659318860f0be24368f583ca51f3d3
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Aug 4 05:51:21 2026 +0000

    docs: adopt standard v2.18.0 — DEPENDENCIES.md and docs/complexity.md

    Two new normative rules land together, and both answer a question this repo
    could previously only answer by running something and reading the error.

    Root DEPENDENCIES.md (documentation-§7). Everything needed to build, run,
    test or release, split runtime / development / release-and-assets, because
    most readers need exactly one of the three. Every entry cites its evidence —
    a file:line, an import, a documented command. The Lua 5.1 requirement is
    stated as a requirement with its reason (tests/_kit/loader.lua uses setfenv,
    removed in 5.2), lizard and luacheck are honestly "any recent". The install
    line for lizard is pipx, not pip: Ubuntu 24.04 marks its Python
    EXTERNALLY-MANAGED and pip refuses outright, so a pip instruction here would
    be broken and look correct. The release section is loud about being optional
    — Pillow for three of the four Python scripts, numpy for artwork_cleaner.py
    alone, and libvulkan1 for the VENDORED upscaler at tools/artwork/bin/, which
    is not installed by any package manager and is verified with ldd rather than
    a --version. Bundled fonts and models are assets, not dependencies, and say
    so.

    docs/complexity.md (performance-§10). Generated with the mandated invocation,
    verbatim, from the repo root. Nine functions warn on CCN and three files sit
    in layout-§1's 1000-1500 band; each carries a one-line disposition, and the
    report says plainly that it is the first one and has nothing to diff against.
    Two dispositions point at work already tracked: F-002 on R.Sanitize and
    PM-007/F-005 around Sl:CliPanel. Artwork.BuildArtSpec (CCN 51) and its file
    are marked peel-next rather than accepted. Closes PM-011.

    The release checkpoint is recorded in docs/testing.md and CLAUDE.md, in both
    cases stating that it happens at RELEASE and is NOT a commit gate — a
    threshold that fails a build is how a collection learns to reach for
    --no-verify.

    Gates after the change: luacheck 0 warnings / 0 errors in 25 files;
    lua tests/run.lua 696 passed, 0 failed.

commit c73a636ae21cefdaaab40bb68d1c1e65bbf8ae00
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Mon Aug 3 19:20:26 2026 +0000

    docs+i18n: adopt standard v2.17.1 — US English spelling throughout

    localization-§5 makes US English the source dialect for every word this
    addon authors, and anti-pattern #46 is the drift that follows when a
    British form survives in a string that later becomes a locale key.

    NO LOCALE KEYS CHANGED. locales/enUS.lua and locales/PostLoad.lua were
    already clean, so nothing in NS.L moved and no call site or test needed
    a matching edit — the failure mode that rule warns about did not arise.

    What moved:
      core/Constants.lua, modules/Canvas.lua   judgement -> judgment
      modules/Canvas.lua                       artefacts -> artifacts
      modules/Registry.lua                     travelled -> traveled
      core/CoreSetup.lua                       synthesised -> synthesized
      tests/test_libka0s.lua                   synthesises -> synthesizes
      tests/test_accent.lua                    fixture panel name "Levelled" -> "Leveled"
      docs/ARCHITECTURE.md                     favours -> favors
      docs/pending/LEDGER.md                   favour/organising/judgement
      docs/superpowers/specs/*.md              favour, behaviour, centred, normalised
      tools/artwork/make_poster.py             colour, centre/centring/centred, unlabelled

    The guard is widened so none of this can come back quietly. The BRITISH
    word list in tests/test_spelling.lua gains the whole -ize family plus
    travell/levell/judgement/artefact/dialogue/practise and the rest of the
    standard's canonical mapping; "analyse"/"paralyse" stay spelled in full
    because "analysis" and "paralysis" are correct US English and a bare
    "analys" stem would flag them.

    Two files the scan did not reach are now covered: docs/pending/LEDGER.md
    and the tools/ generators. The ledger takes a NARROWER form of the
    per-file exemption — a word set rather than a blanket true — because row
    ARTWORK-11 cannot record why the SunnArtPacks exemption exists without
    naming the theme, "Grey"; every other British form in that ledger is
    still a failure there. Widening the scan to the .py generators caught an
    "unlabelled" the manual sweep had missed, which is the argument for the
    guard in one line.

    695 tests pass, luacheck clean. docs/test-cases.md is byte-identical —
    no test NAME changed, only a fixture argument.

commit 3fc345df795fc0afc28afa14655d463034ab47b2
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Mon Aug 3 19:04:43 2026 +0000

    assets: use the pvp_rune_random icon for the addon list

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_0177r5TAPxBMvy7kqskat86N

commit 21a5051fddf77e0fa15f3b85081b7f50544e92b4
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Mon Aug 3 19:04:23 2026 +0000

    docs(claude): state that docs/agent-context.md is gone and must not return

    The Ka0s WoW Addon Standard deleted docs/agent-context.md in v2.17.0
    (documentation-§3, anti-pattern #49), but nothing in this repo said so.
    A fresh agent — /wow-addon:sync-docs in particular — therefore rebuilt the
    pre-v2.17.0 docs set from memory and reported the file as missing.

    Add a section to the root CLAUDE.md naming the three canonical docs inline
    (ARCHITECTURE.md, testing.md, smoke-tests.md) so the membership cannot be
    guessed, stating the prohibition, and flagging that the audit bundles,
    ledgers and plans under docs/ which still name the file are frozen history
    rather than a live requirement.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_0177r5TAPxBMvy7kqskat86N

commit 0503f8e1f703a3aadfced580374cacd0a9881686
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Mon Aug 3 18:42:53 2026 +0000

    docs(ledger): record that ARTWORK-10's premise was verified in-game

    The known-pack manifest exists so a disabled or unloadable SunnArt cannot take the
    feature down with it, and that rests entirely on one behavior: a texture in a
    DISABLED addon's folder still loads. Two commits shipped carrying that unconfirmed.

    It has now been checked in a live client and holds. Recorded on the row a future
    audit actually reads, so the next person finds a settled question rather than a
    load-bearing assumption. docs/smoke-tests.md section 18 step 16 is the repeatable
    form of the check.

    No code or behavior changes. Gate: 695 passed / 0 failed, luacheck 0/0 in 25 files.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_012EpSVgkkKQ3G781F9YP6Gf

commit 0562030b334758c45564c7371dfc0a482d0144e6
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Mon Aug 3 18:34:48 2026 +0000

    feat(artwork): draw Sunn composites, fit panels to art, scale panels

    Closes #12. The renderer the last commit deferred, plus what fell out of finally
    reading SunnArt's own source instead of reasoning about it.

    COMPOSITE RENDERING, as a virtual atlas. A composed bar is treated as ONE image of
    the whole bar's size; BuildArtSpec runs against it completely unchanged, and only
    then is its single rectangle sliced into N. That is the whole design, and it is why
    fill, scale, anchor, crop, flip, rotation, tint, desaturate and blend all keep
    meaning exactly what they mean for a single texture -- the alternative was a second
    set of semantics to define, document and keep in agreement with the first.

      * transformRect is composeUV's screen-space counterpart, and its turn permutation
        is READ OFF composeUV's own corner shuffle rather than derived independently. A
        rotated bar therefore stacks its sections vertically and a horizontal flip
        reverses their order; neither is special-cased anywhere.
      * a band is intersected with the crop the fill already chose, so a FILL that pushes
        the left section off the panel simply does not emit it. The test is against a
        fraction of the span, not `hi > lo`: FILL centers its window, so a crop landing
        exactly on a band edge is the COMMON case, and in floating point the vanishing
        sections otherwise survive by an ulp as sub-pixel slivers that draw as a seam.
      * TILE splits only the horizontal axis -- the vertical repeat stays a REPEAT wrap,
        being inside one file. MAX_ART_QUADS (24) caps the total; over budget the copy
        count drops and each tile grows, so the panel stays COVERED rather than going
        partly bare.
      * every spec carries art.quads and a single texture is a ONE-ELEMENT list, so
        applyArtwork has one path and no branch that can rot.

    THREE CORRECTIONS to what the last commit shipped, all from reading SunnArt 4.01:

      * `overlap` is NOT overlap between sections. Sections are flush (Core.lua:322
        anchors each to the previous one's TOPRIGHT). It is the transparent band at the
        TOP of the artwork (CustomTheme.lua:28-33), which SunnArt hangs over the game
        world and a panel has nothing to hang over. It is cropped instead, so autosize
        and FIT shape a panel around art a player can see. TILE drops the crop: a REPEAT
        wrap repeats a whole file, not a sub-range of one.
      * a precedence inversion. ImportThemes merges panels/overlap in the OPPOSITE order
        to theme names (Core.lua:132-152), so a pack's declared section count beats a
        stale saved-globals entry. Pairing each count table with its name table had it
        backwards. Both orders are now reproduced and asserted.
      * SunnCustomPanels/SunnCustomOverlap are declared in CustomTheme.lua and never read
        by SunnArt, so a player who sets them is silently given 3 sections. We read them,
        ordered last. Kept as a deliberate divergence rather than matched.

    THE KNOWN-PACK MANIFEST (modules/SunnArtPacks.lua, 88 themes from 12 packs). Every
    pack TOC carries `## Dependencies: SunnArt`, enforced before any Lua runs -- so a
    disabled SunnArt means nothing registers and the feature goes silent, while the
    textures remain ordinary drawable files. Sunn has not shipped since 2024-08, so that
    is the expected end state rather than a hypothesis. Live registration still wins per
    theme, and a theme is offered only when its pack folder is actually installed.

    It also fixes ARTWORK-07, which was WRONG. 2:1 was reasoned from Core.lua:259's
    `h = w * scale / 2` -- sound about SunnArt, wrong about the art. Reading the TGA
    headers: 250 of 262 section files are 512x256, but five themes are SQUARE and three
    are 1024 wide. SunnArt draws those squashed; declaring 2:1 would have reproduced its
    bug faithfully. The manifest carries MEASURED dimensions and is consulted for every
    theme it knows, however discovered.

    THE FOLDER GATE APPLIES TO LIVE DISCOVERY TOO, which is not obvious: db.global.themes
    is saved variables and SunnCustomTheme is hand-edited, so both can name a pack deleted
    long ago and put an entry in the dropdown that draws nothing. S.Installed is now
    defined as #S.Themes() > 0 so it cannot claim an install the dropdown then fails to
    populate.

    PER-SECTION ROWS ARE GONE. They turned 88 themes into 270 dropdown entries, four
    fifths of them fragments of something listed three lines above. A single strip is
    still reachable through Custom path. The `-bar` id suffix survives the removal:
    ids are the saved-variables contract, and a rename is breaking whether or not the
    reason for the name still holds.

    FIT TO ARTWORK replaces the artAutosize flag. As a stored mode it reshaped the panel
    on every width change, overwrote a hand-set height, and needed `height` explicitly
    carved out of the re-derive set or typing one undid itself on every keystroke. As a
    button none of that is true and `height` is an ordinary field again. It adopts the
    art's PRESENTED size on both axes -- native size, transposed by rotation, times
    artScale -- which is what STATIC draws at. FIT is deliberately not the target: it
    contains and THEN scales, so fitting to its output would be a fixed point only at
    scale 1 and a shrinking spiral below it.

    PANEL SCALE, a new `scale` field applied by applySpec BEFORE the size and the anchor
    -- a frame's scale is the unit both are expressed in, so setting it afterwards leaves
    the panel one repaint behind. Bounded 0.25-4.0 rather than artScale's 0.1-4.0: that
    one resizes a texture inside a panel whose clickable area is unchanged, this resizes
    the frame, and 0.1 would walk straight around the MIN_SIZE floor that keeps a panel
    grabbable.

    Gate: 695 passed / 0 failed, luacheck 0 warnings / 0 errors in 25 files. Vendor gate
    empty on both readings and on tests/_kit. All three generator --checks clean.
    docs/test-cases.md regenerated and in sync, so the [tests] badge moves 644 -> 695.

    Not verified in a live client, and it is the assumption everything above rests on:
    that a texture in a DISABLED addon's folder still loads. docs/smoke-tests.md section
    18 step 16 tests exactly that.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_012EpSVgkkKQ3G781F9YP6Gf

commit 0562030b334758c45564c7371dfc0a482d0144e6
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Mon Aug 3 18:34:48 2026 +0000

    feat(artwork): draw Sunn composites, fit panels to art, scale panels

    Closes #12. The renderer the last commit deferred, plus what fell out of finally
    reading SunnArt's own source instead of reasoning about it.

    COMPOSITE RENDERING, as a virtual atlas. A composed bar is treated as ONE image of
    the whole bar's size; BuildArtSpec runs against it completely unchanged, and only
    then is its single rectangle sliced into N. That is the whole design, and it is why
    fill, scale, anchor, crop, flip, rotation, tint, desaturate and blend all keep
    meaning exactly what they mean for a single texture -- the alternative was a second
    set of semantics to define, document and keep in agreement with the first.

      * transformRect is composeUV's screen-space counterpart, and its turn permutation
        is READ OFF composeUV's own corner shuffle rather than derived independently. A
        rotated bar therefore stacks its sections vertically and a horizontal flip
        reverses their order; neither is special-cased anywhere.
      * a band is intersected with the crop the fill already chose, so a FILL that pushes
        the left section off the panel simply does not emit it. The test is against a
        fraction of the span, not `hi > lo`: FILL centers its window, so a crop landing
        exactly on a band edge is the COMMON case, and in floating point the vanishing
        sections otherwise survive by an ulp as sub-pixel slivers that draw as a seam.
      * TILE splits only the horizontal axis -- the vertical repeat stays a REPEAT wrap,
        being inside one file. MAX_ART_QUADS (24) caps the total; over budget the copy
        count drops and each tile grows, so the panel stays COVERED rather than going
        partly bare.
      * every spec carries art.quads and a single texture is a ONE-ELEMENT list, so
        applyArtwork has one path and no branch that can rot.

    THREE CORRECTIONS to what the last commit shipped, all from reading SunnArt 4.01:

      * `overlap` is NOT overlap between sections. Sections are flush (Core.lua:322
        anchors each to the previous one's TOPRIGHT). It is the transparent band at the
        TOP of the artwork (CustomTheme.lua:28-33), which SunnArt hangs over the game
        world and a panel has nothing to hang over. It is cropped instead, so autosize
        and FIT shape a panel around art a player can see. TILE drops the crop: a REPEAT
        wrap repeats a whole file, not a sub-range of one.
      * a precedence inversion. ImportThemes merges panels/overlap in the OPPOSITE order
        to theme names (Core.lua:132-152), so a pack's declared section count beats a
        stale saved-globals entry. Pairing each count table with its name table had it
        backwards. Both orders are now reproduced and asserted.
      * SunnCustomPanels/SunnCustomOverlap are declared in CustomTheme.lua and never read
        by SunnArt, so a player who sets them is silently given 3 sections. We read them,
        ordered last. Kept as a deliberate divergence rather than matched.

    THE KNOWN-PACK MANIFEST (modules/SunnArtPacks.lua, 88 themes from 12 packs). Every
    pack TOC carries `## Dependencies: SunnArt`, enforced before any Lua runs -- so a
    disabled SunnArt means nothing registers and the feature goes silent, while the
    textures remain ordinary drawable files. Sunn has not shipped since 2024-08, so that
    is the expected end state rather than a hypothesis. Live registration still wins per
    theme, and a theme is offered only when its pack folder is actually installed.

    It also fixes ARTWORK-07, which was WRONG. 2:1 was reasoned from Core.lua:259's
    `h = w * scale / 2` -- sound about SunnArt, wrong about the art. Reading the TGA
    headers: 250 of 262 section files are 512x256, but five themes are SQUARE and three
    are 1024 wide. SunnArt draws those squashed; declaring 2:1 would have reproduced its
    bug faithfully. The manifest carries MEASURED dimensions and is consulted for every
    theme it knows, however discovered.

    THE FOLDER GATE APPLIES TO LIVE DISCOVERY TOO, which is not obvious: db.global.themes
    is saved variables and SunnCustomTheme is hand-edited, so both can name a pack deleted
    long ago and put an entry in the dropdown that draws nothing. S.Installed is now
    defined as #S.Themes() > 0 so it cannot claim an install the dropdown then fails to
    populate.

    PER-SECTION ROWS ARE GONE. They turned 88 themes into 270 dropdown entries, four
    fifths of them fragments of something listed three lines above. A single strip is
    still reachable through Custom path. The `-bar` id suffix survives the removal:
    ids are the saved-variables contract, and a rename is breaking whether or not the
    reason for the name still holds.

    FIT TO ARTWORK replaces the artAutosize flag. As a stored mode it reshaped the panel
    on every width change, overwrote a hand-set height, and needed `height` explicitly
    carved out of the re-derive set or typing one undid itself on every keystroke. As a
    button none of that is true and `height` is an ordinary field again. It adopts the
    art's PRESENTED size on both axes -- native size, transposed by rotation, times
    artScale -- which is what STATIC draws at. FIT is deliberately not the target: it
    contains and THEN scales, so fitting to its output would be a fixed point only at
    scale 1 and a shrinking spiral below it.

    PANEL SCALE, a new `scale` field applied by applySpec BEFORE the size and the anchor
    -- a frame's scale is the unit both are expressed in, so setting it afterwards leaves
    the panel one repaint behind. Bounded 0.25-4.0 rather than artScale's 0.1-4.0: that
    one resizes a texture inside a panel whose clickable area is unchanged, this resizes
    the frame, and 0.1 would walk straight around the MIN_SIZE floor that keeps a panel
    grabbable.

    Gate: 695 passed / 0 failed, luacheck 0 warnings / 0 errors in 25 files. Vendor gate
    empty on both readings and on tests/_kit. All three generator --checks clean.
    docs/test-cases.md regenerated and in sync, so the [tests] badge moves 644 -> 695.

    Not verified in a live client, and it is the assumption everything above rests on:
    that a texture in a DISABLED addon's folder still loads. docs/smoke-tests.md section
    18 step 16 tests exactly that.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_012EpSVgkkKQ3G781F9YP6Gf

commit 9f4b94b8e5085e92308cbee121873baed769d967
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Mon Aug 3 15:17:31 2026 +0000

    feat(artwork): discover Sunn - Viewport Art packs, and autosize to art

    Toward #12. Two halves of it: user-installed Sunn packs are discovered and
    offered in the artwork dropdown, and a panel can take its shape from whatever
    art it draws. The composite RENDERER is not here yet -- see the caveat below.

    Nothing is redistributed. The player installs SunnArt and whichever packs they
    want; modules/SunnArt.lua only reads what is already on their disk. That is why
    this route carries none of ARTWORK-01's licensing weight -- not one pack byte
    is shipped, copied or hosted.

    ENUMERATION, from the pack's own registration rather than a shipped manifest or
    path probing (the issue's option 1, and its acceptance criterion). SunnArt
    merges four sources and so do we, in its order, later sources winning:

      * SunnArtPack        -- community packs, which SELF-INITIALIZE this global and
                              work with no SunnArt installed at all
      * SunnCustomTheme    -- SunnArt/CustomTheme.lua, hand-edited by the player
      * SunnArt.options.args.theme.values
                           -- where the official packs 1-6 register; SunnArtPack2.lua
                              is five lines of exactly this and nothing else
      * SunnArt.db.global.themes
                           -- themes built in SunnArt's own Advanced options screen

    Read directly, NOT by calling SunnArt:GetThemeList(). That builder mutates
    ThemeDB, sorts its index and fires ImportThemes as a side effect; reading
    another addon's state is safe, but driving its rebuild from our load path would
    make its bugs ours. Verified against an installed SunnArt 4.01 + Art Pack 2 and
    the community SunnArtErgoZPack, and every test fixture is those files' real
    shape rather than one invented to match this code.

    Injection runs at OnEnable, not file scope: a pack is a separate addon whose
    Lua has not necessarily run when our module loads.

    EACH THEME YIELDS BOTH SHAPES, which is the decision the issue left open:

        Sunn -> Art Pack 2: Blackrock            the whole bar
        Sunn -> Art Pack 2: Blackrock (left)     section 1
        Sunn -> Art Pack 2: Blackrock (middle)   section 2
        Sunn -> Art Pack 2: Blackrock (right)    section 3

    Sections are named by POSITION because that is what they are -- SunnArt lays
    them left to right into one bar -- and "2/3" made a player count. Only the ends
    are always unambiguous, so middles are numbered once a theme has more than one.
    The composite wears the bare theme name, so scanning for "Blackrock" finds the
    whole thing and the parts read as qualified variants of it.

    DIMENSIONS are declared, and the assumption is stated rather than buried. No
    pack publishes its pixel size and no WoW API reports a texture file's source
    size -- the wall already documented at Artwork.CUSTOM_NATIVE_SIZE. SunnArt does
    not know either: its `length` is a percentage for pushing art off-screen, not a
    measurement. Every section of every pack inspected is 2:1 (SunnArt's own five
    themes and Art Pack 2's five are all 512x256 -- 30 files; SunnArtErgoZPack is
    512x256 and 1024x256), so 512x256 is declared. Only the ASPECT is consumed, since
    STATIC/FIT/TILE divide panel size by native size, so a 4:1 pack tiles at half the
    frequency a purist wants and nothing worse.

    Artwork rows grew an absolute `path` that wins over the bundled derivation:
    C.ARTWORK_PATH_PREFIX is rooted at THIS addon and cannot reach another's folder.
    Paths are extensionless and rooted at Interface\Addons, matching
    SunnArt_Core.lua:309 -- appending .tga would break any pack shipping .blp.

    AUTOSIZE (artAutosize, off by default) derives a panel's HEIGHT from its width
    and the art's native aspect. One axis, not native pixel size: a bundled piece is
    1024x1024 and a Sunn bar is 1536x256, so adopting native size would throw a wall
    across the screen for one and a letterbox for the other. Square art therefore
    gives 1:1, a Sunn section 2:1, a three-section bar 6:1, a Custom path 1:1.

      * off by default -- it overwrites a hand-set height, and no upgrade should
        reshape a panel someone already had
      * `height` is deliberately NOT a re-derive trigger, or typing a height would be
        undone on every keystroke; autosize reasserts on the next width/art change
      * art that is not installed leaves the shape alone, so uninstalling a pack
        cannot silently reshape a layout
      * runs BEFORE Sanitize, so the derived height meets the same MIN/MAX clamp

    Artwork.NativeSize is the one place that answers "how big is this piece", so the
    autosize seam and the fill math cannot disagree.

    CAVEAT, and the reason this is not "Closes #12": applyArtwork still draws one
    texture, so a composed row currently renders its LEFT section stretched across
    the panel instead of all sections edge to edge. Canvas creates a single f.art at
    frame-build time (modules/Canvas.lua:210); the composite needs frame
    construction, applyArtwork and the pool reset to carry N textures, scoped to
    these rows alone. Autosize already measures the full bar, so the composed row's
    proportions are right ahead of the renderer that will fill them. Docs land with
    that commit rather than describing a half-drawn feature now.

    Gate: 644 passed / 0 failed, luacheck 0 warnings / 0 errors in 24 files. Vendor
    gate empty on both readings and on tests/_kit; both artwork --checks green.
    docs/test-cases.md regenerated and in sync at 644, so the [tests] badge moves
    618 -> 644.

    Not verified in a live client: that a real pack's textures resolve in-game. The
    paths are asserted against SunnArt's own construction, not against a load.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_012EpSVgkkKQ3G781F9YP6Gf

commit 475ff5fcbd61d0edfc63f38f41e973fbc2483ae5
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Mon Aug 3 14:34:46 2026 +0000

    feat(artwork): generate a deterministic poster of the bundled set

    Closes #14. One PNG showing every artwork that ships, grouped under its
    catalog category, embedded in the README and ready for the CurseForge media
    tab. tools/artwork/make_poster.py draws it.

    It does not walk media/artwork/ itself. It imports update_catalog.py and calls
    that script's scan(), sorts by the SORT_KEY that script now exports, and shares
    its fatal-problem policy through is_fatal() -- so the poster and the dropdown
    are two renderings of one list and cannot disagree about what shipped. Two
    walks would have been two chances to drift, and the drift would be invisible: a
    poster showing art the addon does not have still looks fine.

    Determinism is stated about PIXELS, not file bytes, because only one of those
    is ours. Five things are pinned so the pixels cannot move: fonts resolved only
    from tools/artwork/fonts/ with no system fallback, the text layout engine
    forced to BASIC, every string placed on a whole pixel, the stamp taken from the
    TOC version rather than a clock, and every loop running over the sorted rows.
    The file's bytes are deflate's output and a zlib-ng build can compress
    identical pixels differently, so --check fingerprints the decoded image and a
    plain run rewrites nothing when the pixels already match. A contributor on a
    different toolchain therefore cannot churn 2 MB of visually identical binary
    into the history. Verified across Pillow 10.2.0/FreeType 2.13.2 and Pillow
    11.3.0/FreeType 2.13.3 -- same fingerprint, --check green on both.

    That leaves one input a script cannot pin: a FreeType whose glyph
    rasterization changes would move the pixels themselves. It is recorded instead.
    media/poster/artwork-poster.txt carries the fingerprint, the font hashes and
    the three library versions of the build that made the committed image, and
    --check names the component that differs when a mismatch appears.

    The fonts are vendored at tools/artwork/fonts/ rather than media/fonts/: media/
    is scoped by the standard to SHIPPED assets and these never ship, being
    packaged out by .pkgmeta's `tools` ignore. Recorded as ARTWORK-06 with the
    rationale, and repeated in the module docstring, because the risk is not that
    the choice is wrong but that a fast read of layout-§3 "fixes" it later.

    .pkgmeta ignores media/poster on the same reasoning as the logo renders and the
    raw plates -- WoW cannot load a .png at all, so shipping one would add
    megabytes to every download for a file the client physically cannot use.

    The poster is stamped with the TOC version, which means a version bump stales
    it; docs/testing.md, docs/artwork-spec.md and the README all say so. No
    mandated gate can detect that staleness -- luacheck walks only Lua and the
    harness derives its load list from the TOC and cannot shell out to Python -- so
    both --check commands are written down in docs/testing.md ▸ The artwork gate,
    explicitly outside the green gate on the precedent testing-§7 sets. The
    exposure is ARTWORK-05; it is pre-existing on the catalog side, where
    update_catalog.py --check had been documented nowhere.

    Doc drift fixed while here: three files claimed the artwork scripts all use
    numpy when only artwork_cleaner.py imports it; ARCHITECTURE named a
    tools/artwork/import.py that does not exist; testing.md and the LEDGER cited a
    tests/perf.lua that is a construct of the standard, not a file in this repo.

    Gate: 618 passed / 0 failed, luacheck 0 warnings / 0 errors in 23 files, both
    --check commands green, vendor gate empty on both readings and on tests/_kit.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_012EpSVgkkKQ3G781F9YP6Gf

commit 475ff5fcbd61d0edfc63f38f41e973fbc2483ae5
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Mon Aug 3 14:34:46 2026 +0000

    feat(artwork): generate a deterministic poster of the bundled set

    Closes #14. One PNG showing every artwork that ships, grouped under its
    catalog category, embedded in the README and ready for the CurseForge media
    tab. tools/artwork/make_poster.py draws it.

    It does not walk media/artwork/ itself. It imports update_catalog.py and calls
    that script's scan(), sorts by the SORT_KEY that script now exports, and shares
    its fatal-problem policy through is_fatal() -- so the poster and the dropdown
    are two renderings of one list and cannot disagree about what shipped. Two
    walks would have been two chances to drift, and the drift would be invisible: a
    poster showing art the addon does not have still looks fine.

    Determinism is stated about PIXELS, not file bytes, because only one of those
    is ours. Five things are pinned so the pixels cannot move: fonts resolved only
    from tools/artwork/fonts/ with no system fallback, the text layout engine
    forced to BASIC, every string placed on a whole pixel, the stamp taken from the
    TOC version rather than a clock, and every loop running over the sorted rows.
    The file's bytes are deflate's output and a zlib-ng build can compress
    identical pixels differently, so --check fingerprints the decoded image and a
    plain run rewrites nothing when the pixels already match. A contributor on a
    different toolchain therefore cannot churn 2 MB of visually identical binary
    into the history. Verified across Pillow 10.2.0/FreeType 2.13.2 and Pillow
    11.3.0/FreeType 2.13.3 -- same fingerprint, --check green on both.

    That leaves one input a script cannot pin: a FreeType whose glyph
    rasterization changes would move the pixels themselves. It is recorded instead.
    media/poster/artwork-poster.txt carries the fingerprint, the font hashes and
    the three library versions of the build that made the committed image, and
    --check names the component that differs when a mismatch appears.

    The fonts are vendored at tools/artwork/fonts/ rather than media/fonts/: media/
    is scoped by the standard to SHIPPED assets and these never ship, being
    packaged out by .pkgmeta's `tools` ignore. Recorded as ARTWORK-06 with the
    rationale, and repeated in the module docstring, because the risk is not that
    the choice is wrong but that a fast read of layout-§3 "fixes" it later.

    .pkgmeta ignores media/poster on the same reasoning as the logo renders and the
    raw plates -- WoW cannot load a .png at all, so shipping one would add
    megabytes to every download for a file the client physically cannot use.

    The poster is stamped with the TOC version, which means a version bump stales
    it; docs/testing.md, docs/artwork-spec.md and the README all say so. No
    mandated gate can detect that staleness -- luacheck walks only Lua and the
    harness derives its load list from the TOC and cannot shell out to Python -- so
    both --check commands are written down in docs/testing.md ▸ The artwork gate,
    explicitly outside the green gate on the precedent testing-§7 sets. The
    exposure is ARTWORK-05; it is pre-existing on the catalog side, where
    update_catalog.py --check had been documented nowhere.

    Doc drift fixed while here: three files claimed the artwork scripts all use
    numpy when only artwork_cleaner.py imports it; ARCHITECTURE named a
    tools/artwork/import.py that does not exist; testing.md and the LEDGER cited a
    tests/perf.lua that is a construct of the standard, not a file in this repo.

    Gate: 618 passed / 0 failed, luacheck 0 warnings / 0 errors in 23 files, both
    --check commands green, vendor gate empty on both readings and on tests/_kit.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_012EpSVgkkKQ3G781F9YP6Gf

commit 6c793515cefed256259f5d09d10b1ff4a7196b51
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Mon Aug 3 09:21:17 2026 +0000

    fix(panels): stamp the frame name at create, so a rename keeps it

    Closes #6, #7 and #3. The first two were one root cause wearing two faces.

    Canvas.BuildSpec derived frameName from rec.name on every render, so a rename
    produced a different frame name -- and a frame's name is immutable after
    CreateFrame. Canvas:Render therefore had to retire the old frame and acquire a
    new one, which orphaned every external anchor pointed at the old global (#7)
    and parked a frame in the pool that nothing would ever claim again (#6). One
    permanently-live frame per distinct name the user typed.

    The frame name is now stamped onto the record at create as rec.frameName and
    never recomputed. That makes it identity, alongside id and name:

      * Registry.FrameName reads the stored field, falling back to the derived
        name for records that predate it
      * R:Reset preserves it through the template rewrite; COPY_EXCLUDED keeps
        CopyFrom from handing one panel another's global; create stamps it after
        the overrides loop, so no caller can supply one
      * R:Rename does not touch it -- and its frame-name collision check is GONE,
        because a rename claims no new global. Renaming to a name that merely
        slugs the same as another panel ("Chat-BG" while "Chat BG" exists) is now
        allowed, where it used to be refused.

    Schema v1 -> v2 (NS:RunMigrations) stamps every existing panel by deriving from
    its name, which reproduces exactly the global the previous build already gave
    that frame, so the upgrade moves nobody's anchors. R.Sanitize fills the field
    the same way for a record arriving by another route -- an imported profile, a
    test -- which is what covers profiles other than the one active at migration.

    #3 was independent and small: R:Reset rewrites a record from C.PANEL_TEMPLATE,
    which deliberately carries no preview marker, so resetting a placeholder
    stripped its marker and promoted it to a permanent panel that survived the next
    sweep -- the last path by which test mode could leave litter in a real layout.
    Reset now refuses a marked record with a reason the caller prints, per the
    issue's own recommendation. CopyFrom needed no guard; COPY_EXCLUDED already
    held the marker. A test walks every write seam in sequence to pin that the
    marker cannot be lost.

    DELIBERATE CONTRACT CHANGE, and the reason both issues' acceptance criteria
    allowed for it: after a rename the frame name no longer matches the panel's
    name and stops being derivable from it. Predictability at create is the lesser
    half of that contract -- you look the name up once, when you wire something to
    the panel, and the name box's tooltip is where. Anchors that silently stop
    tracking days later are the worse failure. A second consequence, recorded as a
    known limitation: creating a panel can be refused by a name that looks free,
    because a panel created as "Alpha" and since renamed still holds
    PanelMaster_Panel_Alpha. The refusal names the holder.

    Docs synced alongside: both old Known-limitations bullets deleted and replaced
    by the two the new behavior actually has; ARCHITECTURE's "Frame names and the
    pool" rewritten around identity rather than derivation; the panel record and
    its identity fields; the preview-marker section; README anchoring, settings
    table and troubleshooting; smoke tests 2, 5c/5d/5e, 11b. Three items of
    pre-existing drift fixed while there: the tests badge said 605 against a
    609-test suite, and three smoke steps described a "Frame name" line in the
    editor that has been a tooltip on the Panel name box for some time.

    Gate: 618 passed / 0 failed, luacheck 0 warnings / 0 errors in 23 files.
    Vendor gate empty on both readings and on tests/_kit. docs/test-cases.md
    regenerated and in sync at 618, so the [tests] badge moves 605 -> 618.

    Not verified in a live client: that a real frame's GetName() survives a rename
    and a /reload. That is smoke tests 11b.5-8, which need a session.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_012EpSVgkkKQ3G781F9YP6Gf

commit 6c793515cefed256259f5d09d10b1ff4a7196b51
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Mon Aug 3 09:21:17 2026 +0000

    fix(panels): stamp the frame name at create, so a rename keeps it

    Closes #6, #7 and #3. The first two were one root cause wearing two faces.

    Canvas.BuildSpec derived frameName from rec.name on every render, so a rename
    produced a different frame name -- and a frame's name is immutable after
    CreateFrame. Canvas:Render therefore had to retire the old frame and acquire a
    new one, which orphaned every external anchor pointed at the old global (#7)
    and parked a frame in the pool that nothing would ever claim again (#6). One
    permanently-live frame per distinct name the user typed.

    The frame name is now stamped onto the record at create as rec.frameName and
    never recomputed. That makes it identity, alongside id and name:

      * Registry.FrameName reads the stored field, falling back to the derived
        name for records that predate it
      * R:Reset preserves it through the template rewrite; COPY_EXCLUDED keeps
        CopyFrom from handing one panel another's global; create stamps it after
        the overrides loop, so no caller can supply one
      * R:Rename does not touch it -- and its frame-name collision check is GONE,
        because a rename claims no new global. Renaming to a name that merely
        slugs the same as another panel ("Chat-BG" while "Chat BG" exists) is now
        allowed, where it used to be refused.

    Schema v1 -> v2 (NS:RunMigrations) stamps every existing panel by deriving from
    its name, which reproduces exactly the global the previous build already gave
    that frame, so the upgrade moves nobody's anchors. R.Sanitize fills the field
    the same way for a record arriving by another route -- an imported profile, a
    test -- which is what covers profiles other than the one active at migration.

    #3 was independent and small: R:Reset rewrites a record from C.PANEL_TEMPLATE,
    which deliberately carries no preview marker, so resetting a placeholder
    stripped its marker and promoted it to a permanent panel that survived the next
    sweep -- the last path by which test mode could leave litter in a real layout.
    Reset now refuses a marked record with a reason the caller prints, per the
    issue's own recommendation. CopyFrom needed no guard; COPY_EXCLUDED already
    held the marker. A test walks every write seam in sequence to pin that the
    marker cannot be lost.

    DELIBERATE CONTRACT CHANGE, and the reason both issues' acceptance criteria
    allowed for it: after a rename the frame name no longer matches the panel's
    name and stops being derivable from it. Predictability at create is the lesser
    half of that contract -- you look the name up once, when you wire something to
    the panel, and the name box's tooltip is where. Anchors that silently stop
    tracking days later are the worse failure. A second consequence, recorded as a
    known limitation: creating a panel can be refused by a name that looks free,
    because a panel created as "Alpha" and since renamed still holds
    PanelMaster_Panel_Alpha. The refusal names the holder.

    Docs synced alongside: both old Known-limitations bullets deleted and replaced
    by the two the new behavior actually has; ARCHITECTURE's "Frame names and the
    pool" rewritten around identity rather than derivation; the panel record and
    its identity fields; the preview-marker section; README anchoring, settings
    table and troubleshooting; smoke tests 2, 5c/5d/5e, 11b. Three items of
    pre-existing drift fixed while there: the tests badge said 605 against a
    609-test suite, and three smoke steps described a "Frame name" line in the
    editor that has been a tooltip on the Panel name box for some time.

    Gate: 618 passed / 0 failed, luacheck 0 warnings / 0 errors in 23 files.
    Vendor gate empty on both readings and on tests/_kit. docs/test-cases.md
    regenerated and in sync at 618, so the [tests] badge moves 605 -> 618.

    Not verified in a live client: that a real frame's GetName() survives a rename
    and a /reload. That is smoke tests 11b.5-8, which need a session.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_012EpSVgkkKQ3G781F9YP6Gf

commit ed850dace980e874d7a233d2869f75e886d58e14
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Sun Aug 2 18:33:30 2026 +0000

    feat(artwork): tint every piece, add Desaturate and Blend mode

    The artwork color control now applies to all 101 bundled pieces instead of
    almost none of them. It used to exist only while the selected art was
    "tintable", and every piece the new pipeline generates is full-color — so in
    practice the control appeared for exactly two selections, None and Custom path.
    It read as a setting that did not exist.

    Removing the gate on its own would have been worse than leaving it. Tinting
    finished art is a multiply, so a blue tint on a gold-and-crimson crest returns
    muddy brown rather than blue, which is precisely what the old force-white
    branch was protecting against. DESATURATE is what makes it work: it drains the
    art to grayscale in hardware first, so the tint multiplies against neutral gray
    and comes back as a clean, saturated version of the chosen color. The default
    tint is white — a no-op — so nothing changes for anyone who has not picked one.

    BLEND MODE returns as a two-item setting, Normal and Glow. It was removed once
    on the reasoning that MOD needs transparency to be WHITE while ADD needs it
    BLACK, and one texture cannot satisfy both. That stalemate is over: every plate
    artwork_cleaner.py produces normalizes transparent pixels to (0,0,0,0), so black
    is the answer and ADD is correct by construction. The other three modes stay
    out — MOD is now permanently wrong, DISABLE ignores alpha outright, and ALPHAKEY
    hard-edges the art while defeating the opacity slider. Blizzard's UI has used
    only BLEND and ADD since 2.2.0, and WeakAuras ships exactly these two.

    `tintable` is therefore gone entirely: from the generator, the catalog rows, the
    tests and the docs. Its only consumer was the branch this removes.

    The editor's artwork section is re-laid-out, which also fixes a wrap. The color
    pair emits TWO half-width widgets — the swatch and its Class color companion —
    so sharing a row with the opacity slider put three half-width controls on one
    line and the third wrapped underneath. It now has a row to itself. Artwork and
    Custom texture path go full width, the latter because catalog labels carry their
    whole derived category and truncate to nothing at half width. And the pair being
    unconditional retires makeArtColorRow, which released and rebuilt widgets and
    re-laid-out the scroll frame whenever you paged between tintable and non-tintable
    art, shoving every row below it up and down.

    Two unrelated fixes ride along, both reported from the game:

    Panel dropdowns sort by name. Registry:All() returns creation order, which is
    right for storage and arbitrary for a list you have to find a name in. Sorted on
    a COPY at the two display sites — All() hands back the live saved-variables
    array, so sorting in place would reorder the user's file and shift the index
    Registry:FindByName returns alongside its record.

    A new panel's accent color defaults to white rather than the BenikUI green, which
    looked deliberate without anyone having chosen it. Class color is still on by
    default, so the white shows once that is unticked.

    Gate: 609 pass, luacheck 0/0, catalog in sync with 101 files on disk.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01AHJypyBccFqBfgds4bQBY7

commit cb334493a29cb19118015f1eb9b626945d1066a2
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Sun Aug 2 15:05:20 2026 +0000

    Re-vendor LibKa0s v1.5.0 (DebugLog minor 7)

    DebugLog's gated sink now pcalls its string.format, so a combat secret logged
    through a numeric slot lands as a line instead of raising inside the sink. A
    satisfiable format renders byte-for-byte as at minor 6, so nothing this addon
    prints changes.

    diff -r and diff -r --strip-trailing-cr against ../LibKa0s/LibKa0s are both
    empty. Suite unchanged at 607 passed, 0 failed; luacheck 0/0.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01PXMksCakSxnoNEcxSgDK47

commit 3a52a603338ccb220412dc6a24a8a473bf12d284
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Sun Aug 2 15:02:55 2026 +0000

    fix(artwork): delete wiki_import.py, left behind by the rename

    The rewrite created artwork_cleaner.py as a new file and removed import.py,
    plate.py and manifest.tsv — but never removed wiki_import.py, the very file
    artwork_cleaner.py replaces. It shipped as a broken duplicate: it still does
    `from plate import ...`, and plate.py is gone, so it raises ModuleNotFoundError
    on any invocation.

    Nothing referenced it. Caught by listing tools/artwork/ after the merge rather
    than by anything automated, which is the gap worth noting: the suite gates Lua,
    and a dead Python file in tools/ is invisible to it.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01AHJypyBccFqBfgds4bQBY7

commit 1d36934a3d2c707dbc9bc5c25f302b05c896014f
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Sun Aug 2 15:01:38 2026 +0000

    refactor(artwork): one pipeline, driven by the folder tree

    Replaces two importers and a manifest with two scripts and no configuration.
    The generated-plate route is gone entirely — `import.py`, its white-on-black and
    magenta-chroma keys, and every AI-generation prompt in the docs. What remains
    handles art that already exists, whatever its source.

    `artwork_cleaner.py` converts, in two modes: `--single FILE` writes
    FILE-panelmaster.tga beside its source, `--batch SRC DST` mirrors a whole tree.
    `update_catalog.py` then reads media/artwork/ and rewrites the catalog block in
    modules/Artwork.lua from what is actually on disk.

    The manifest is gone because the folder tree can carry everything it did.
    media/artwork/faction/expansion/12-midnight/harati.tga yields the id, the file
    stem, the label "Harati" and the category "Faction -> Expansion -> 12 Midnight".
    Renaming a file renames it in the UI; moving it regroups it. There is nothing to
    keep in sync because there is only one place the answer lives.

    That makes categories DYNAMIC, which is the change that reaches into the addon.
    C.ARTWORK_CATEGORIES and its membership set are deleted, and Artwork.List now
    sorts categories alphabetically rather than against a declared rank. Alphabetical
    is not a fallback: a child's category string starts with its parent's, so folders
    stay adjacent to their children for free, and nothing needs editing when a folder
    appears.

    `tintable` is measured rather than declared. Art whose opaque pixels are both
    unsaturated and near-white carries its shape in the alpha channel and takes a
    tint cleanly; anything else has its own palette and forces the tint to white.
    Both conditions are required — mid-gray line art is unsaturated but not tintable,
    because gray times a tint returns a dark desaturated tint instead of the tint.

    `credit` is dropped from the catalog row; attribution lives in the README.

    Two fixes prompted by running it in anger. The cleaner narrated nothing while the
    CPU upscaler worked, which is indistinguishable from a hang — every stage now
    prints as it starts, with timings, and the upscaler's own progress is streamed
    rather than swallowed. And a scale requirement just above a power of four chained
    an entire extra pass: a 184x255 source needing 4.02x built a 2944x4080
    intermediate, twelve million pixels, nearly all discarded on the way back to
    1024. One pass already reaches 736x1020, four pixels short, so the chain now
    stops when Lanczos can cover the remainder within 5%. That file went from 56.5s
    to 4.6s for the same output.

    modules/Artwork.lua's catalog is now a generated block between markers; the prose
    and logic around it stay hand-owned. tests/test_artwork.lua moves off the deleted
    seed row onto a real shipped one, and the assertions that encoded the old
    declared-category ordering are rewritten for the derived one.

    Gate: 607 pass, luacheck 0/0, catalog in sync with 101 files on disk.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01AHJypyBccFqBfgds4bQBY7

commit 1d36934a3d2c707dbc9bc5c25f302b05c896014f
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Sun Aug 2 15:01:38 2026 +0000

    refactor(artwork): one pipeline, driven by the folder tree

    Replaces two importers and a manifest with two scripts and no configuration.
    The generated-plate route is gone entirely — `import.py`, its white-on-black and
    magenta-chroma keys, and every AI-generation prompt in the docs. What remains
    handles art that already exists, whatever its source.

    `artwork_cleaner.py` converts, in two modes: `--single FILE` writes
    FILE-panelmaster.tga beside its source, `--batch SRC DST` mirrors a whole tree.
    `update_catalog.py` then reads media/artwork/ and rewrites the catalog block in
    modules/Artwork.lua from what is actually on disk.

    The manifest is gone because the folder tree can carry everything it did.
    media/artwork/faction/expansion/12-midnight/harati.tga yields the id, the file
    stem, the label "Harati" and the category "Faction -> Expansion -> 12 Midnight".
    Renaming a file renames it in the UI; moving it regroups it. There is nothing to
    keep in sync because there is only one place the answer lives.

    That makes categories DYNAMIC, which is the change that reaches into the addon.
    C.ARTWORK_CATEGORIES and its membership set are deleted, and Artwork.List now
    sorts categories alphabetically rather than against a declared rank. Alphabetical
    is not a fallback: a child's category string starts with its parent's, so folders
    stay adjacent to their children for free, and nothing needs editing when a folder
    appears.

    `tintable` is measured rather than declared. Art whose opaque pixels are both
    unsaturated and near-white carries its shape in the alpha channel and takes a
    tint cleanly; anything else has its own palette and forces the tint to white.
    Both conditions are required — mid-gray line art is unsaturated but not tintable,
    because gray times a tint returns a dark desaturated tint instead of the tint.

    `credit` is dropped from the catalog row; attribution lives in the README.

    Two fixes prompted by running it in anger. The cleaner narrated nothing while the
    CPU upscaler worked, which is indistinguishable from a hang — every stage now
    prints as it starts, with timings, and the upscaler's own progress is streamed
    rather than swallowed. And a scale requirement just above a power of four chained
    an entire extra pass: a 184x255 source needing 4.02x built a 2944x4080
    intermediate, twelve million pixels, nearly all discarded on the way back to
    1024. One pass already reaches 736x1020, four pixels short, so the chain now
    stops when Lanczos can cover the remainder within 5%. That file went from 56.5s
    to 4.6s for the same output.

    modules/Artwork.lua's catalog is now a generated block between markers; the prose
    and logic around it stay hand-owned. tests/test_artwork.lua moves off the deleted
    seed row onto a real shipped one, and the assertions that encoded the old
    declared-category ordering are rewritten for the derived one.

    Gate: 607 pass, luacheck 0/0, catalog in sync with 101 files on disk.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01AHJypyBccFqBfgds4bQBY7

commit 4dcddf013a2049bba0e11abe7237e44fbf5639fc
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Sun Aug 2 13:40:48 2026 +0000

    feat(artwork): the converted wiki set, in a source-mirroring layout

    104 imported plates land under media/artwork/, and the tree now mirrors the
    download folder they came from — media/artwork/class/warrior.tga against
    raw/class/Warrior_Crest.png — so the two can be diffed and read as the same
    thing. The four originally generated plates and their raw sources are removed.

    This replaces grouping by catalog category, which put the same art in a
    different place from its source and made the two trees impossible to compare.
    The consequence is worth stating plainly: **directory and `category` are now
    unrelated**. The directory answers "where did this come from"; the row's
    `category` answers "which dropdown group does this appear under". A piece filed
    at faction/expansion/12-midnight/ is a Factions row. Nothing derives one from
    the other.

    `wiki_import.py` emits the new layout rather than the old one, via
    `output_stem()` reading the manifest's own `source` column — without which the
    next run would have silently recreated the category folders and undone this.
    `--emit-catalog` follows, so `file` now carries nested backslash segments
    (`faction\expansion\12-midnight\harati`), which `modules/Artwork.lua:128`
    concatenates verbatim exactly as before. `--only` additionally accepts the
    on-disk stem or a bare subject, since the catalog id and the file path no longer
    coincide. All 104 emitted rows were parsed with `lua` and checked against disk:
    0 structural problems, 0 missing files.

    KNOWN RED: `lua tests/run.lua` is 606/607. `modules/Artwork.lua` still carries
    the four original catalog rows, whose files this commit deletes, so the
    path-existence assertion fails. Swapping in the 104 new rows touches 43
    references in tests/test_artwork.lua — which uses `runic-sigil-bw` as its
    fixture throughout and asserts it is the tintable, square seed row, a property
    none of the 104 has — so it is a deliberate follow-up rather than a rename.
    `luacheck .` remains 0/0.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01AHJypyBccFqBfgds4bQBY7

commit c914b5b50e19feb338c0d04886ea05d6b18fd4ff
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Sun Aug 2 12:42:59 2026 +0000

    feat(artwork): a batch import pipeline for art that already has alpha

    `import.py` converts one generated plate that has NO alpha and must have some
    derived — by luminance, or by a magenta chroma key. Downloaded art is the
    opposite problem: 104 of 107 Warcraft Wiki sources arrive WITH a real alpha
    channel, and running `to_tintable` or `chroma_key` over them would throw that
    alpha away and re-derive a worse one from brightness. Hence a sibling script
    rather than a flag, sharing only its downstream half via the new `plate.py`.

    Output moves to `media/artwork/<category>/<subject>.tga`, 1024x1024 letterboxed,
    32-bit RLE. No addon code changes: `modules/Artwork.lua:128` already composes
    `C.ARTWORK_PATH_PREFIX .. row.file .. ".tga"`, so a `file` stem of
    `classes\warrior` resolves with no resolver change, and the existing
    path-existence test validates every row for free.

    The stage that matters most is `solidify`. Sources disagree about what RGB sits
    under a transparent pixel — (0,0,0,0) on most, (255,255,255,0) across the
    MajorFactionsIcons set, (217,182,103,0) on Human_Crest.png. Nothing renders it,
    so nothing complains; but an upscaler's kernel samples those pixels, and a
    white-under-alpha source grows a white halo along every emblem edge.

    Manifest-driven because catalog ids are written into saved variables and can
    never be renamed. Names are reviewed as data before any file is written, and
    every drop the scaffold makes on the author's behalf is printed.

    Measured, not assumed: the vendored upscaler runs on software Vulkan (llvmpipe)
    and preserves alpha; the full 104-asset batch is ~33 minutes, not the 1-3 hours
    first estimated. One model for the whole catalog rather than per-scale
    selection — the latter measured 1.7x faster and was rejected because these
    assets sit adjacent in a dropdown and three models sharpen differently.

    Hardened against silent failure, which is what a long unattended run cannot
    afford. A `--scaffold` at a mistyped path used to overwrite the manifest with a
    bare header and exit 0, destroying every hand-measured erase box; it now
    validates, refuses to orphan hand-authored rows, and writes atomically. The
    up-to-date check compared source mtime alone, so an edited erase box or a
    changed --size left a stale TGA in place and reported success — replaced with a
    fingerprint over every input that determines the output bytes.

    `import.py` output verified byte-identical across all three conversion paths
    before and after the extraction. Three accepted deviations from the Ka0s WoW
    Addon Standard recorded in docs/pending/LEDGER.md as ARTWORK-01..04.

    No artwork is converted or catalogued by this commit; `media/` is untouched.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01AHJypyBccFqBfgds4bQBY7

commit 4abaac4773118e3928f1665e210466e083b19c12
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Sun Aug 2 10:18:28 2026 +0000

    test: gate the vendored payload against the LibKa0s tag the README names

    This repo carried LibKa0s and its test kit with nothing checking that either
    matched a release. It did not stay theoretical: the kit's first numbered
    revision was vendored here straight off LibKa0s master, while that revision was
    in no tag, and the README provenance line went on naming a release the bytes no
    longer matched. Nothing here noticed. prettychat was the only repo in the
    collection with this check, it refused the same vendor, and LibKa0s v1.4.0 was
    cut so the revision could be taken from a tag.

    Two cases, ported from prettychat's tests/test_harness.lua: libs/LibKa0s and
    tests/_kit must each match `git show <tag>:...` in the sibling library repo,
    file set first and then byte for byte, README.md included — the file that
    actually diverged in this collection was a README.

    The tag comes from THIS README's provenance line rather than a constant, so a
    line and a payload that disagree is the failure rather than something the check
    is blind to. It compares against the released tag, never the sibling's working
    tree: LibKa0s can be mid-release, and reddening this suite for upstream work
    this addon has not adopted invites a "fix" that ships bytes existing at no ref.

    The one place it may go quiet is a missing sibling checkout, and that is said in
    the case name instead of hidden. Verified both ways before landing: a one-line
    edit to a vendored file fails it by name, and a provenance line rolled back to
    v1.3.1 fails it too.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01573xwgHgpvTJwWGQHqGogs

commit 4348bc5ae46b70d9ec7fd39a392ed228b3d77ac8
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Sun Aug 2 10:05:49 2026 +0000

    docs: name LibKa0s v1.4.0 in the provenance line

    The vendored library is unchanged — v1.4.0's payload is byte-identical to
    v1.3.1 and no LibStub minor moved. What moved is tests/_kit, which took the
    kit's first numbered revision before that revision was in any release, leaving
    this line naming a tag its vendored tree no longer matched.

    The line is not decoration: prettychat's harness resolves the tag it names and
    asserts both vendored trees against it file by file, which is how the drift was
    found. Pointing it at v1.4.0 makes it true again.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01573xwgHgpvTJwWGQHqGogs

commit 6754148b6ed9d3678b1f8b2a0dc7f5ab90c8f2a8
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Sun Aug 2 08:51:12 2026 +0000

    chore(tests): re-vendor the test kit at revision 1

    The shared harness under tests/_kit/ now carries a revision number —
    Kit.VERSION at the top of framework.lua, reaching suites as KIT_VERSION — so
    this addon can name which copy of the kit it is running rather than leaving
    that answerable only by diffing against LibKa0s at the right commit.

    Nothing in this addon's own code or tests changes, and nothing negotiates on
    the number: the kit is not a LibStub library and the vendoring gate is still
    byte-identity. Its full surface is documented at
    https://github.com/tusharsaxena/LibKa0s/tree/master/docs/api/testkit

    Vendored copy — never edit tests/_kit/ here. A kit problem is a finding to fix
    in LibKa0s and re-vendor.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01573xwgHgpvTJwWGQHqGogs

commit 1a88d3b5532bf7e53f0f3ac2e1c5fc33d05af1da
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Sun Aug 2 06:42:48 2026 +0000

    docs: delete the scaffolding pack; it is fetched at runtime now

    WowAddonStandards v2.17.0 removes docs/agent-context.md from the canonical
    docs/ set (documentation-§3, the CRITICAL subsection) and makes shipping it
    anti-pattern #49. The file here was the Ka0s Addon Context Pack v2.7.0,
    dropped in at scaffold time.

    The reason it has to go rather than be maintained: it is scaffolding, and
    every question it answers -- kickstart walkthrough, starter tree, starter
    snippets, "definition of done for v0.1.0" -- was answered the moment this
    addon existed. Worse, it is the file CLAUDE.md sent every session to FIRST,
    so it is loaded as working context and its instructions get followed. A
    stale copy does not go quiet, it actively misleads: this one still carried
    a hand-written debug console, a hand-written slash dispatcher and a
    hand-written harness in an addon that replaced all three with LibKa0s two
    commits ago. No gate can see that -- no test covers a doc and lint does not
    read prose. The pack is one curl away and always current.

    Migrated OUT before deleting, one item, the only thing in the file that was
    genuinely about THIS repo and not already recorded elsewhere:

      * the shared test kit, into docs/testing.md ▸ How the harness works.
        tests/_kit/ is vendored from ../LibKa0s/testkit and owns the registry,
        the assertions, the --list renderer and the loader; run.lua is a thin
        consumer deriving its load order from the TOC; wow_mock.lua EXTENDS
        _kit/mock_base.lua rather than replacing it, because the base is the
        only source of a LibStub with a real NewLibrary (without which no
        vendored major registers headlessly) and of a fireable AceGUI. That
        section still listed a tests/loader.lua deleted in the adoption, so the
        file tree is corrected in the same edit.

    Everything else was already covered and is dropped rather than duplicated:
    the LibKa0s adoption, the never-edit-libs/ rule and the seam load order are
    in CLAUDE.md and ARCHITECTURE.md ▸ The LibKa0s seams; the green gate and the
    vendor gate are in docs/testing.md. The rest is the pack's own generic
    content -- Ka0s identity facts, the layout boilerplate, the TOC/entry/
    Compat/Locale/Database/Settings/luacheckrc/pkgmeta snippets, the hard-rules
    and forbidden-pattern lists that restate the standard verbatim, the v0.1.0
    definition of done, the patterns table and the skills list -- deleted
    outright, which is what documentation-§3 requires.

    References fixed: the CLAUDE.md pointer list (documentation-§2 item 4 now
    forbids naming it), ARCHITECTURE.md's "read this first" line, and
    tests/test_spelling.lua's authored-doc list, which scans a hardcoded set and
    would have gone red on the missing file. The frozen dated bundles under
    docs/audits/, docs/reviews/ and the LEDGER's DOC-08 row still name it and
    are deliberately untouched: they are records of what was true on a past day.

    -620 lines.

    Gate: 605 passed / 0 failed, luacheck 0 warnings / 0 errors in 23 files.
    Suite count unchanged and docs/test-cases.md still in sync at 605, so the
    [tests] badge does not move.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01AHJypyBccFqBfgds4bQBY7

commit 06fd61272b6a7fb03a95b605fd959b499fa8f5c5
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Sun Aug 2 06:29:01 2026 +0000

    docs(artwork): tell the generator to start from scratch, not to edit a reference

    Three prompt refinements to the asset-authoring spec, carried over from
    working with the generators in practice:

      * "Generate a new image from scratch" up front on both plate prompts.
        Without it, handing a service a reference image gets an EDIT of that
        image rather than a new emblem in the same language -- which fails the
        B&W plate outright, since the whole point is a clean alpha key.
      * "Use the attached image as a reference (DO NOT COPY)" where a
        reference is being supplied, so the style transfers and the subject
        does not.
      * the third prompt's [SUBJECT] placeholder drops its inline examples, so
        all three read identically and the substitution is the only thing a
        contributor changes.

    Three mechanical slips fixed in passing, not rewording: a missing
    apostrophe in "dont", an unbalanced "DO NOT COPY)", and a missing blank
    quote line that would have rendered the reference line and the Style block
    as one paragraph in the third prompt where the other two separate them.

    Docs only; no code, no assets. This change predates the LibKa0s adoption
    and was deliberately kept out of it.

    Gate: 605 passed / 0 failed, luacheck 0 warnings / 0 errors in 23 files.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01AHJypyBccFqBfgds4bQBY7

commit c124a247093e87d3a140757ae8321afa3cd3b465
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Sun Aug 2 06:20:21 2026 +0000

    docs: point the agent brief at LibKa0s instead of at hand-built copies

    docs/agent-context.md is the file CLAUDE.md sends every session to first,
    and it had zero mentions of LibKa0s. An agent following it would hand-write
    the debug console, the slash dispatcher, the options shell and the harness
    loader that the adoption just deleted -- which is the most expensive kind
    of doc drift this repo can carry.

      * a new "The shared library" section: what is vendored, which majors are
        adopted, and the three rules with no exceptions (never edit libs/ or
        tests/_kit/, re-vendor whole-folder, every seam degrades through the
        shared NS.LIBKA0S_MISSING clause).
      * the starter TOC gains the LibKa0s line with the ordering reason, and
        the core/ and settings/ blocks gain the three seam files.
      * four "patterns to reproduce" rows now say the library owns them rather
        than describing how to rebuild them: the console, the harness, the lazy
        Defaults button and the lazy first-OnShow body.
      * two orphaned references cleared -- tests/loader.lua (deleted in favour
        of the kit's) and ensureDefaultsButton(panel) (now O.EnsureDefaultsButton).

    CLAUDE.md gains the vendoring rule and names the vendor gate as a third
    check, because neither `lua tests/run.lua` nor `luacheck .` can see a stale
    libs/ -- the library's own suite passes against the library, and this
    addon's passes against a stale copy that still works.

    No code touched. README.md needed nothing: its COMMANDS <-> documented-verb
    parity is 18/18 in both directions, and it carries no module map or file
    list to go stale. No dead exports found.

    Gate: 605 passed / 0 failed, luacheck 0 warnings / 0 errors in 23 files,
    both halves of the vendor diff empty.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01AHJypyBccFqBfgds4bQBY7

commit c124a247093e87d3a140757ae8321afa3cd3b465
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Sun Aug 2 06:20:21 2026 +0000

    docs: point the agent brief at LibKa0s instead of at hand-built copies

    docs/agent-context.md is the file CLAUDE.md sends every session to first,
    and it had zero mentions of LibKa0s. An agent following it would hand-write
    the debug console, the slash dispatcher, the options shell and the harness
    loader that the adoption just deleted -- which is the most expensive kind
    of doc drift this repo can carry.

      * a new "The shared library" section: what is vendored, which majors are
        adopted, and the three rules with no exceptions (never edit libs/ or
        tests/_kit/, re-vendor whole-folder, every seam degrades through the
        shared NS.LIBKA0S_MISSING clause).
      * the starter TOC gains the LibKa0s line with the ordering reason, and
        the core/ and settings/ blocks gain the three seam files.
      * four "patterns to reproduce" rows now say the library owns them rather
        than describing how to rebuild them: the console, the harness, the lazy
        Defaults button and the lazy first-OnShow body.
      * two orphaned references cleared -- tests/loader.lua (deleted in favour
        of the kit's) and ensureDefaultsButton(panel) (now O.EnsureDefaultsButton).

    CLAUDE.md gains the vendoring rule and names the vendor gate as a third
    check, because neither `lua tests/run.lua` nor `luacheck .` can see a stale
    libs/ -- the library's own suite passes against the library, and this
    addon's passes against a stale copy that still works.

    No code touched. README.md needed nothing: its COMMANDS <-> documented-verb
    parity is 18/18 in both directions, and it carries no module map or file
    list to go stale. No dead exports found.

    Gate: 605 passed / 0 failed, luacheck 0 warnings / 0 errors in 23 files,
    both halves of the vendor diff empty.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01AHJypyBccFqBfgds4bQBY7

commit fd7e535b33c2579b8dfccc9d0745519876ff7904
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Sun Aug 2 06:01:47 2026 +0000

    docs(libka0s): the vendor gate, the seam contract, and four smoke sections

    Perf is DECLINED (LIBKA0S-31), and the reason is what this addon does
    rather than a judgement about thresholds. Perf measures two combat-gated
    windows differing only in whether the host is inert; PanelMaster draws
    static backdrop panels. Every event it registers is PLAYER_LOGIN,
    PLAYER_ENTERING_WORLD or PLAYER_REGEN_ENABLED -- none fires DURING combat
    -- and every repaint path is driven by the user having the options panel
    open, which options-ui-§2 refuses in combat outright. So every bucket but
    one would read 0.000 by construction. The exception is the shared mouseover
    ticker: 10 Hz, one MouseIsOver and one SetAlpha per tracked panel, a
    per-panel opt-in that is off by default. Buying that one near-noise-floor
    figure would mean putting a Perf.suspended check inside the renderer's own
    show decision, since the host contract requires visibility be enforced at
    the source. Real code in the render path for a diagnostic that cannot
    answer a question.

    docs/testing.md gains The vendor gate: the four diffs run in pairs, with a
    table saying what each of the three possible answers means and what to do.
    Neither green gate can see a stale vendored copy -- the library's suite
    passes against the library, and this addon's passes against a stale copy
    that still works. It also records that luacheck's figure is scoped, not
    repo-wide.

    docs/ARCHITECTURE.md gains a section on the seams and the load order they
    pin, with NS.LIBKA0S_MISSING documented as a CROSS-FILE CONTRACT three
    other seams depend on rather than an implementation detail of one file --
    including the table of what each seam appends to it.

    docs/smoke-tests.md gains §14-§17 for what no headless suite can reach: the
    degraded install (rename libs/LibKa0s aside, confirm zero Lua errors, a
    complete listing, the notice said exactly once, and the cause clause
    matching the other adopters word for word), the L-trap sweep across every
    page and both console windows, the nine deliberate rendered changes with a
    parity list beside them framed as "nothing moved", and the destructive
    path's confirmation on BOTH entry points.

    Gate: 605 passed / 0 failed, luacheck 0 warnings / 0 errors in 23 files,
    both halves of the vendor diff empty for libs/LibKa0s and tests/_kit, and
    the [tests] badge, docs/test-cases.md and the suite all agree at 605.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01AHJypyBccFqBfgds4bQBY7

commit ee80646bfeb37bbaafa5bc8d7a4180fcccdd5a7d
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Sun Aug 2 05:58:35 2026 +0000

    feat(libka0s): adopt LibKa0s-Options-1.0 for the settings canvas

    settings/OptionsSetup.lua (135 lines) holds the descriptor; NS.Helpers IS
    the library instance rather than a wrapper around one (options-ui-§1),
    which is what lets settings/Panel.lua decorate it in place. Panel.lua goes
    691 -> 492 lines, and a large share of what remains is prose recording what
    moved.

    Taken outright: CreatePanel (with the header, the breadcrumb and the
    Blizzard canvas contract), EnsureDefaultsButton, EnsureScroll,
    PatchAlwaysShowScrollbar, Section, AddSpacer, AttachTooltip, ClearScroll,
    RenderField, RenderSchema, SetRenderer, RegisterOptionsPage,
    CreateOptionsPanel, OpenOptionsPanel, RefreshScalars and lib.LAYOUT.

    Four surfaces are knowingly declined with reasons (LIBKA0S-26):
    RestoreAllDefaults (this addon's global reset must reach session-only rows
    through each row's own set), InlineButtonPair (it owns its row; this
    addon's paired buttons go INTO a row someone else is building), LSMValues
    and the colour codecs (no schema row here is media-backed or colour-typed).

    Two adapters, both wrapped ON THE INSTANCE rather than beside it, and the
    distinction is load-bearing: RenderRows resolves RenderField and
    EnsureScroll from the instance table at call time, so a plain host helper
    is bypassed by every page the flow engine draws. A test caught exactly that
    for real -- the dropdown-close hooks landed on the landing page and the
    panel editor, and were missing from the one page that has a dropdown.

    The registration also gains what it did not have: each page builder is
    pcall'd separately, so a raising one costs only itself and is reported by
    key rather than killing every page after it; and every page gets
    SetRenderer's combat guard, which matters because the Blizzard AddOns
    sidebar reaches a panel without going through /pm config.

    The schema -> widget -> write path had NO coverage before this and now has
    eight cases (LIBKA0S-29). This addon's AceGUI mock returned nil from
    Create(), so the whole path was unreachable; the kit's fireable widgets
    make it testable, and a mutation immediately found that the General page
    rendered through RenderRows with an explicit list and left the descriptor's
    rowsForPage never called. Switched to RenderSchema.

    One test assertion changed to accommodate a deliberate library behaviour
    and is recorded as such (LIBKA0S-28): OnDefault is a forwarder, not an
    assignment, so the case moves from identity to behaviour.

    Five mutations were run against the new assertions; all five went red with
    the expected message once two bad mutations were corrected.

    Gate: 605 passed / 0 failed, luacheck 0 warnings / 0 errors in 23 files,
    both halves of the vendor diff empty.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01AHJypyBccFqBfgds4bQBY7

commit cd660f0487c43d6bf1daa38268b59505923028b3
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Sun Aug 2 05:50:01 2026 +0000

    feat(libka0s): adopt LibKa0s-Slash-1.0 and converge both user-visible surfaces

    The dispatcher, the help renderer, the landing-page row formatter, the
    schema CLI (list/get/set/reset/resetall/version), the type-aware parser and
    FormatKV all move to the library. settings/Slash.lua keeps what the library
    has no business owning: NS.COMMANDS, and every PANEL verb -- those act on
    registry records rather than schema rows.

    NS.COMMANDS flips from keyed {name=,desc=,fn=} to the positional
    {name, description, handler} triples the library reads. Eighteen entries,
    the dispatcher, the help renderer, the landing page and seven test cases
    moved together. The table stays this addon's and is passed in rather than
    owned: the landing page renders the same rows, and a library that owned it
    would force the options major to resolve the slash major to read it, which
    is a real dependency cycle between two majors at load time.

    settings/Schema.lua moves to the library's row vocabulary -- type "boolean"
    becomes "bool" (6 rows) and `options` becomes `values` (1 row). An unmapped
    field here is not an error but something worse: a `set` that answers
    "unknown setting type", and a row that will silently vanish from the page
    once Options lands.

    Both convergences are ADOPTED and recorded (LIBKA0S-22, -23):

      * `reset` takes a path. Already true here before the adoption, which is
        not the same as "not applicable" -- both verbs now delegate to the
        library, no working command was removed, and the one destructive verb
        this addon has (`/pm panel deleteall`) is not a schema reset and keeps
        its confirmation on both entry points.
      * one command-row formatter. settings/Panel.lua's landing page carried a
        second one for the same NS.COMMANDS data, and the two had already
        drifted: double spaces around the em dash, the dash white-wrapped, the
        description bare. Found by following the render path, not by grepping
        for a name.

    Five rendered changes a user will notice, all pinned by tests and recorded
    at LIBKA0S-17: uppercase colour escapes, an em dash in the help header,
    two-space indents on `/pm help` rows, two-line parse refusals, and an
    out-of-range number that clamps instead of being refused.

    Two descriptor adapters, both host-shaped and both closures in the seam
    file: groupKey (this schema groups by row.group, the library defaults to
    row.page) and parse (the library matches an enum case-sensitively, and
    `/pm set settings.defaultStrata low` has always worked here).

    The hand-maintained LIST_GROUP_ORDER constant is deleted rather than ported:
    it named the three groups in the order they are already declared in, and its
    own comment admitted that a name matching nothing would fail invisibly.

    Five mutations were run against the new assertions. Four went red as
    expected; the fifth found a VACUOUS pre-existing test -- the lower-case enum
    case set "low" against a row whose default is already LOW, so it passed
    whether the parse adapter existed or not. Rewritten to move off the value
    first, and it now catches the mutation.

    Gate: 596 passed / 0 failed, luacheck 0 warnings / 0 errors in 22 files,
    both halves of the vendor diff empty.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01AHJypyBccFqBfgds4bQBY7

commit 51682f31b15dc81cc0b02a43d6a6db817695a14b
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Sun Aug 2 05:42:54 2026 +0000

    feat(libka0s): adopt LibKa0s-DebugLog-1.0 and delete modules/DebugLog.lua

    modules/DebugLog.lua (429 lines) deletes outright; core/DebugLogSetup.lua
    (208) replaces it. Net -221 lines for the same console.

    The swap is faithful by construction rather than by care: both formatters
    were ALREADY byte-identical to the library's, MAX_BUFFER was already 500,
    and the frame globals the descriptor derives from `name` --
    PanelMasterDebugWindow and PanelMasterDebugCopyWindow -- are exactly the two
    this addon hardcoded, so nothing anchored to them moves. The 25 cases in
    tests/test_debuglog.lua pass unchanged against the library instance.

    Two survivors have no library equivalent and stay: NS.DebugBuild (the gated
    sink for a call site whose arguments cost something to produce) and
    D:Diagnose() (the structured dump verb, which reports what THIS addon
    believes is on screen). Diagnose is defined once and attached on both the
    present-library and degraded paths, so `/pm debug dump` answers either way.

    The console moves from modules/ up into core/: every host reference in the
    descriptor is a closure, so nothing resolves at load and no dependency
    inverts.

    Two user-visible changes, both deliberate (LIBKA0S-11, -12):

      * the console wears Core.SKIN -- a flat 1px black edge, a 1px light-gray
        inner highlight, a gold title, a gray divider -- instead of the plain
        background texture this file painted. As of Core minor 3 the Ka0s window
        edge is the library's rather than each host's, so the collection's
        consoles read as one suite of addons.
      * it wears Core's x rather than the flat "X" drawn here. Pinned through
        _frameForTest.titleBarOffsets, which are derived from the button's width
        and are the only readable evidence of which factory built it.

    And one bug fixed by the adoption, recorded so it is not mistaken for drift
    (LIBKA0S-13): the old console synced the settings checkbox inline from
    D:Show()/D:Hide(), but the window is in UISpecialFrames, so closing it with
    Esc never went through D:Hide and left the checkbox stale. The library's
    onVisibilityChanged fires from the frame's own OnShow and OnHide.

    DebugLog is one of the three majors that CAN express the L trap, so it gets
    a real rendered assertion rather than a tripwire. Four mutations were run
    against the new assertions; all four went red with the expected message.

    Gate: 591 passed / 0 failed, luacheck 0 warnings / 0 errors in 22 files,
    both halves of the vendor diff empty.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01AHJypyBccFqBfgds4bQBY7

commit ca2de21423da8eeb483de56f13fd8f92d8f2707a
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Sun Aug 2 05:37:33 2026 +0000

    feat(libka0s): adopt the shared test kit and LibKa0s-Core-1.0

    Vendors LibKa0s v1.3.1 whole-folder into libs/LibKa0s/ and the shared test
    kit into tests/_kit/, then takes the first of the five majors.

    The kit comes FIRST, before any library module, for one reason: mock_base.lua
    is the only source of a LibStub with a real NewLibrary, and without it no
    vendored library can register headlessly at all — every seam would silently
    take its degraded path while the suite stayed green.

      * tests/wow_mock.lua becomes an EXTENDER over tests/_kit/mock_base.lua
        rather than being replaced by it. Ten base behaviours are deliberately
        overridden, each named in that file's header with its reason; the two
        taken from the base are the LibStub above and a FIREABLE AceGUI, which
        replaces a mock whose Create() returned nil and left the whole
        schema -> widget -> write path untestable.
      * tests/loader.lua is deleted in favour of the kit's, and tests/run.lua
        derives the addon's load order from PanelMaster.toc instead of keeping a
        second hand-maintained copy of it.
      * core/CoreSetup.lua holds the LibKa0s-Core-1.0 seam. The secret-safe guard
        and the prefixed chat printer leave core/Util.lua (-34 lines). NS.Print,
        NS.Util.print, NS.SafeToString and NS.IsConcatSafe keep their names --
        six files take the printer as a file-scope upvalue -- and the AceConsole
        reclaim in core/PanelMaster.lua stays load-bearing and correct.
      * NS.LIBKA0S_MISSING is the shared cause clause every later seam appends
        its own consequence to, set outside the `if not lib` branch because they
        read it on both paths.
      * Core's window-chrome half is declined: this addon's only standalone
        window is the debug console, which LibKa0s-DebugLog-1.0 will draw.

    tests/test_libka0s.lua proves the seam is real rather than merely working:
    it asserts function IDENTITY against the live library, and drives the
    degraded install by loading the addon with libs/LibKa0s absent rather than
    by hand-stubbing a branch. Seven mutations were run against the new
    assertions and all seven went red with the expected message.

    tests/test_harness.lua closes the kit's silently-green hole, where a listed
    suite with no file on disk is skipped rather than failed.

    Gate: 583 passed / 0 failed, luacheck 0 warnings / 0 errors in 22 files,
    and both halves of the vendor diff empty for libs/LibKa0s and tests/_kit.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01AHJypyBccFqBfgds4bQBY7

commit c3ebd12adcd00f38b53551462670cf540d13428a
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Jul 31 20:40:00 2026 +0000

    docs(pending): track issue numbers in the ledger and close two settled rows

    The ledger's dedup rule needs a machine-readable link from a row to the
    GitHub issue covering it — without one, an issue found by the next sweep
    gets raised a second time alongside the row it already belongs to. Add
    the Issue column and backfill all 28 rows from their rationales.

    Also record the untriaged state in the legend, so a row that was merely
    seen can never be misread as a decision, and settle the two git rows
    whose deferrals the user has since resolved by hand: GIT-01 (origin is
    level with HEAD) and GIT-03 (the artwork paths landed in 47f507a).

    ISS-10, the load-on-demand artwork packs, is deferred against issue #10.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_012in5BXk49W7V9hKUiGP1WM

commit 50e0073f085586f30f4ae9eb6af5afb1c881b7a7
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Jul 31 20:22:46 2026 +0000

    chore: drop the artwork generator; art is authored by hand now

    Artwork is authored outside the repo from here on, so the procedural
    Pillow generator has no remaining caller. tools/artwork/import.py is
    now the only artwork tool, and every doc that pointed at generate.py
    points at the importer instead.

    The frozen design spec keeps its generator section, marked superseded,
    rather than being rewritten -- it is the record of what shipped on the
    feat/panel-artwork branch, not instructions.

    media/artwork/runic-sigil-bw.tga stays in the catalog. It was the one
    asset the generator produced and it has no plate under
    media/artwork/raw/, so it is no longer re-derivable from the repo.

    Also retargets the GIT-01 ledger locator at origin/master after the
    default branch was renamed from main. The evidence hash is unchanged:
    the Source column is a locator, not the hashed evidence text.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_012in5BXk49W7V9hKUiGP1WM

commit 47f507acecb994983b445fd9029556fc1d142e72
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Jul 31 19:45:10 2026 +0000

    feat(artwork): draw per-panel artwork inside a panel's bounds

    Closes #9.

    A panel can carry a picture as well as a color, from a bundled catalog or a
    texture path of the user's own. Off by default (artTexture "None"), so no
    existing panel or profile changes appearance on upgrade.

    modules/Artwork.lua owns the catalog and BuildArtSpec, the pure record-to-
    geometry function. It touches no frames and calls no WoW API, which is what
    makes all five fill types verifiable headlessly against resize -- the part of
    this most likely to be wrong and least visible when it is.

    Quarter turns transpose the art's effective dimensions, not just its texture
    coordinates. Rotating only the UV quad squashed STATIC, FIT, FILL and TILE at
    90 and 270 degrees; square art hides it entirely, so the fill matrix now runs
    wide and tall art against wide and tall panels at every rotation.

    The background fill moves onto its own child frame so artwork can be drawn
    behind it -- a child frame always draws above its parent's textures. Two
    consequences had to be handled: frame level outranks draw layer, so the fill
    began covering the unlock outline (the overlay now has its own frame above the
    whole ladder), and a panel's footprint grew from three rungs to eight, so panel
    levels are strided to stop adjacent ones interleaving.

    Blend mode was built and then removed. DISABLE ignores alpha, so art defined by
    its alpha channel can only ever draw as a rectangle; MOD needs transparent
    regions white while ADD needs them black, and one texture cannot be both. Two
    of five options could only produce bug reports, so the setting is gone and
    artwork always draws with normal transparency.

    564 tests pass (79 new), luacheck clean, in-client smoke tests in
    docs/smoke-tests.md 5e-5e-6 pass.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_0153GTF527WasaoCrontHQ1d

commit e6cc65e62a345538d3d5b1c9648d15abd392b0bd
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Jul 31 08:50:13 2026 +0000

    docs(pending): record the pending-audit decision ledger

    Adds the ledger /wow-addon:pending-audit maintains, so a settled item stops
    re-surfacing on every sweep. Rows are matched on ID plus an evidence hash, so
    editing the underlying comment, plan row or issue title correctly re-opens the
    question.

    Covers the 2026-07-31 sweeps: three limitations filed as issues #6-#8, the
    release-blocked audit steps and publishing deviations, the deferred
    localization pass, and the panel-artwork issue #9.

    Decisions are written as marker plus word (green done, blue wont-do, yellow
    deferred) with the legend in the file itself — the word is the data so it
    survives grep and a screen reader, the marker is the affordance.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_0153GTF527WasaoCrontHQ1d

commit 224e945cbda1dba399f93ac5782bbe41ecef673c
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Jul 31 08:17:36 2026 +0000

    refactor(debug): defer expensive log arguments past the sink's gate

    C-08 removed the restated `if NS.State.debug` gate from fourteen call
    sites but left two, because their arguments are not free to produce:
    Registry:Set wrapped R.FormatField (a string.format on a seam that fires
    on every field write) and Unlock:SetPanelUnlocked wrapped Registry:Get
    (an O(n) scan). Both guarded the arguments, not the sink — so the fix was
    never a second gate, it was a way to defer the arguments.

    NS.DebugBuild(tag, fmt, build, ...) calls `build` only after NS.Debug's
    own gate. Both sites now use it with a plain file-local builder
    (describeWrite, describeLock), so `grep -rn "NS.State.debug"` outside
    modules/DebugLog.lua is empty and C-08's exit criterion is met literally
    rather than by documented exception.

    The builder is a plain function reference with its arguments passed
    unbound, deliberately not a closure. A closure capturing upvalues is
    allocated at the CALL SITE, before NS.Debug is entered, so the obvious
    `function() return f(x) end` shape would have paid the exact cost the
    deferral exists to avoid — strictly worse than the gate it replaced. The
    off case is now one call and one boolean test, allocating nothing. A test
    pins the unbound-argument shape so the closure form cannot creep back.

    The four sites left un-gated (Database migrate, Canvas RenderAll,
    Registry ReloadProfile, Panel safeRun) stay un-gated and are ratified as
    intended; any can move to DebugBuild if its cost ever changes.

    Docs: ARCHITECTURE gains a Localization section and module-map rows for
    locales/enUS.lua and locales/PostLoad.lua — the only two loaded files it
    never named. NS.L, the key-is-the-string fallback, the "no string routes
    through NS.L in 0.1.0" scope decision and the two never-localize rules
    (panel names are user data; point/strata are stable tokens) existed only
    as comments inside those files.

    The review bundle records the decisions taken on 2026-07-31: the two
    gates and the four un-gated sites are closed, PanelEditor.lua's 814 LOC
    is accepted against C-07's pre-peel ~500 estimate, F-018 is recorded as
    did-not-reproduce, and R:DeleteBatch and offsetRange's relPoint keying
    are ratified. Deferrals B2/B3/B5 moved to GitHub issues #3, #4 and #5.

    Gate: lua tests/run.lua 477/477 (was 474), luacheck . 0/0.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_0153GTF527WasaoCrontHQ1d

commit 26b7739596c3e85b8eb8bba7093308b6892b03ee
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Jul 31 07:58:23 2026 +0000

    fix(canvas): rotate the accent texture on left and right edges

    A LibSharedMedia statusbar texture is authored as a horizontal bar: its
    height carries the bevel, its width is the fill direction. Stretched into
    a tall thin left or right bar with no rotation, that bevel ran along the
    bar's length instead of across its thickness, so a vertical bar read as a
    smear while the top and bottom bars looked correct.

    The vertical edges now turn the texture a quarter turn, via the
    eight-argument SetTexCoord — the only form that can transpose the axes;
    the four-argument form crops and flips but cannot rotate. Left and right
    take the same rotation rather than mirrored ones, matching top and bottom,
    which are also drawn identically.

    Two details the tests pinned down:
    - the coords are re-applied on every repaint, not once at creation.
      SetTexture resets a texture's coords, and frames are pooled, so a frame
      reused for another panel would inherit the previous orientation.
    - top and bottom set the flat coords explicitly rather than leaving them
      alone, so no prior state can leak in.

    SetTexCoord was not modeled in tests/wow_mock.lua at all, so texture
    orientation was previously untestable; it now records what it was told,
    the same way SetColorTexture and SetVertexColor already do.

    Docs synced alongside: the README's Edges row mentions the rotation and
    its tests badge moves 471 -> 474 (docs/testing.md requires the badge and
    test-cases.md move together).

    Gate: lua tests/run.lua 474/474, luacheck . 0/0.

    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_0153GTF527WasaoCrontHQ1d

This mod has no additional files