promotional bannermobile promotional banner

Ka0s KickCD

Tracks interrupt + CC cooldowns on a movable icon grid
Back to Files

1.3.0-release

File nameKickCD-1.3.0-release.zip
Uploaded
Sep 10, 2026
Downloads
23
Size
6.4 MB
Flavors
Retail
File ID
8853252
Type
R
Release
Supported game versions
  • 12.1.0

What's new

commit ed88759f694600b3ad6aaf3281f6a4b60c795622
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Sep 10 23:54:57 2026 +0530

    Release 1.3.0

    The global cooldown is now attributed per spell rather than per log line (#15),
    so icons stop reading as ready when they are not; the cast bar stopped minting
    a closure per cast; and the download is about 7.5 MB lighter. 176 commits since
    the 1.2.1 tag.

    Release gate passed on all four suites at 1fee098: luacheck 0/0 across 94
    files, 870 tests green, 6 perf scenarios, zero functions above CCN 15. The
    largest source growth of the nine this cycle (+359 NLOC over 20 new functions),
    and the averages confirm it is new code rather than existing code thickening.
    Evidence: docs/automated-tests/20260910-234511/.

    What's new is deliberately not recreated (documentation-§1, standard v2.42.0).
    Badges follow: WoW to 12.1.0, tests to the 870 measured.
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01G4Xy2KrRa8NmD1843rgKnE

commit 566f6fc3eddb5fe20b5e642256770f4fe1abef24
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Sep 11 00:55:19 2026 +0530

    sync-docs: three dead publications, and a gate that guarded nothing

    Const.PANEL_DEFAULTS_W, Helpers.PrintSchemaError and Helpers.FireOnChange had no
    callers. The first two are simple deletions; FireOnChange took its file-local
    with it, because the publication was the local's only consumer and luacheck said
    so the moment the line went. That in turn left tests/test_locale.lua excusing an
    'onChange for ' literal that no longer exists in settings/Panel.lua, and the
    residue gate caught it -- the entry is dropped. Panel_Render.lua's entry stays;
    that literal is still there.

    tests/test_spelling.lua asserted that libs/LibKa0s/LibKa0s.lua is not reached by
    the spelling scan. No such file exists in the vendored payload, so the assertion
    passed on a path that could never be scanned -- vacuously true, guarding nothing.
    It now names libs/LibKa0s/Core.lua, which is real.

    tests/test_locale.lua pointed a reader at docs/debug.md for the DIAGNOSTIC class
    wording; ARCHITECTURE.md:214 files that doc Not applicable and it does not exist.
    The clause is gone.

    Target interface to 120100 across DEPENDENCIES.md, ARCHITECTURE.md, scope.md and
    module-map.md. docs/test-cases.md regenerated -- it was one case stale.

    luacheck clean at 94 files, 870 tests green.
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01G4Xy2KrRa8NmD1843rgKnE

commit 1fee09855098b757a525a7968cdeb5c2dc06c806
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Sep 10 23:44:53 2026 +0530

    Target patch 12.1.0 — the TOC interface becomes 120100

    Retail moved to 12.1.0, and an addon whose TOC still declares 120007 shows as
    out of date in the client's addon list even though nothing in it broke.

    The line is the only thing that changed. LibKa0s needed nothing alongside it:
    it ships no TOC of its own, reads the client build from GetBuildInfo() at
    runtime rather than declaring one, and the harness vendored as tests/_kit/
    carries no build number at all — so no re-vendor was involved.

    Verified before committing: luacheck clean and the headless suite green in
    every repo.

    Living documentation still says 120007 / 12.0.7 in the README badge and
    DEPENDENCIES.md, and in several repos ARCHITECTURE.md, scope.md, module-map.md
    and smoke-tests.md as well. That sweep is /wow-addon:sync-docs' job, not this
    one-line change's. Comments recording measured 12.0.7 behavior stay as they
    are — they are still true.
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01G4Xy2KrRa8NmD1843rgKnE

commit 8d76ff16d3362434b61a157fd262bd581a4f3061
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Sep 10 23:26:19 2026 +0530

    README: drop the logo and the five settings-panel screenshots

    Five shots of a settings panel tell a reader nothing about whether the addon
    is worth installing; the zoomed grid-and-cast-bar shot does that on its own.
    The logo block goes too, since the CurseForge page carries it. The opening
    question folds into the paragraph that answers it rather than standing alone.

    The logo bitmap is refreshed to match the upload.
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01G4Xy2KrRa8NmD1843rgKnE

commit 0d30e0610cc52506f3469af8c610e1cce9e9596f
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Sep 10 03:31:45 2026 +0530

    media: refresh the screenshot set

    Local source images only. Every README serves its screenshots from the
    CurseForge CDN, so nothing here is referenced by a doc and no page
    changes until the new set is uploaded there.

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

commit 1e59f87a21b939b2d0f60f4e3cb13b6402445837
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Sep 10 02:51:40 2026 +0530

    README: drop `## What's new` (standard v2.42.0)

    documentation-§1 no longer carries the section. It was bound to the top
    `## Version History` row -- the two "MUST agree" and rolled forward
    together on every bump -- so its content was a copy of another MUST's
    content by definition, and the only work it could generate was keeping
    the copy true. Version History is now the addon's single player-facing
    history.

    Anti-pattern #40 is retired in place upstream; the item list renumbers
    6-13 to 5-12.

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

commit 2eb8755b8ee4dcec1a631a9c8f24a545ed8c9232
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Sep 9 21:43:07 2026 +0530

    README: adopt the standard's prose Usage section (v2.41.0)

    documentation-§1 item 7 no longer mandates the two tables that made
    Usage a contents page. The `Command | What it does` table said what
    exists without ever saying what to do, and it was a second copy of a
    list the addon already generates from NS.COMMANDS for `/<slash> help`,
    so it could only be in lockstep or wrong. The `Tab | Covers` table has
    moved to docs/settings-panel.md, its canonical home, where the finer
    page-tab-row tree already sat beneath it.

    Usage is now prose in the order a new player meets the addon: how the
    display is shown, hidden, moved and locked, what preview or test mode
    is for, what the chrome's controls do, and the core interactions. It
    closes on one sentence pointing at Settings > AddOns and the slash
    command list.

    The whole README was humanized in the same pass -- em dashes cut,
    `**Bold lead.**` bullets removed, see-saw pairs and repeated rhetorical
    moves broken up, sentence and paragraph length varied.

    Reviewed by an adversarial pass that checked every factual claim
    against source; the corrections it found are folded in.

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

commit a3ee0856fa22f93b77f2194277108211debb86db
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Sep 9 19:46:22 2026 +0530

    README: the Troubleshooting table takes the header the standard names

    `documentation-§1` item 10 states the section as a **Symptom | Fix**
    table, and this one said `| Symptom | What to check |`. Header row only -- not a symptom or a
    fix is reworded.

    Found while restyling Ka0s Multi Meters, whose FAQ and Troubleshooting
    were paragraphs rather than tables. Nine addons carry both sections; the
    FAQ header was already unanimous, and four of the nine had drifted on
    this one.

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

commit 70b17b14a2ec1b32a5ea2626a8e5c73ff8f2a3d5
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Sep 9 03:21:46 2026 +0530

    perf(cooldowns): bracket the SPELL_STATE publish, and observe the nesting

    The 20260909-014035 capture put a 9.64 ms pass inside Cooldowns:Refresh and
    outside every child bracket, and the buckets could not say where. This is what
    was in the gap.

    `spellState` brackets the SUBSCRIBER (IconGrid:OnSpellState); nothing bracketed
    the NS:SendMessage around it — CallbackHandler's dispatch plus the six-field
    payload table the loop allocates per emitting spell, ~1590 of them across that
    capture's 810 passes. A collection landing inside the bracket is charged to
    whichever pass was unlucky, which is the shape a lone outlier with no child
    spike actually has; tests/perf.lua measures spellPoll at 528 bytes/iter against
    spellState's 209. `stateEmit` now brackets the publish and the constructor with
    it, so the chain is spellPoll > stateEmit > spellState > iconApply and
    spellPoll minus its two children is the bare loop.

    NOT glowGate, which the capture's write-up first proposed: RefreshAllGlows is
    not reachable from Cooldowns:Refresh. The only glow work on the poll path is
    Icon:UpdateGlow, already inside iconApply. glowGate is declared anyway on its
    own merits — four of its five call sites sit inside no bracket, so
    LibCustomGlow's cost has been invisible in every capture — and declares no
    parent, for the reason PerfSetup.lua already records for `visibility`.

    parentKey is threaded through pollSpell, stateEmit and iconApply so containment
    is measured rather than asserted. PollSpell and Icon:Apply take it as an
    argument rather than naming a parent themselves: Rebuild polls the same spells
    from outside spellPoll, and two of Apply's three callers run under no bracket,
    so a hard-coded parent would report containment in a pass that never ran — the
    same unverified claim the change exists to end.

    tests/test_perfsetup.lua pins both directions, including that a Rebuild-driven
    poll claims no parent. Red under dropping any third argument. luacheck 0/0,
    870 tests pass.

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

commit f704e4123c56b8e9711dc210ee43c82612cea2e3
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Sep 9 03:21:31 2026 +0530

    perf: the 2026-09-09 capture — one 9.64 ms pass, unattributable

    A party-of-five run in Nexus-Point Xenas, v1.2.1. The frame-time delta came
    back at +0.04 ms/frame, roughly a seventh of the instrument's floor, so it
    resolves nothing; the buckets are the result.

    Accounted cost is 3.674 ms per second of combat, which is unremarkable. The
    finding is the tail: one spellPoll pass cost 9.6400 ms — 37x its own mean and
    69.7% of a 13.83 ms frame — with no child bucket anywhere near it (pollSpell
    max 0.2961, spellState 0.2135, iconApply 0.1948). The cost was inside
    Cooldowns:Refresh and outside every bracket it contained, so the record can
    locate the hitch to a function and no further.

    Every nested row reads "declares itself within X — not observed", so the
    residual that isolates the gap is arithmetic over an unverified tree. Both
    gaps are closed in the commit that follows this one.

    Line endings normalised to the CRLF .gitattributes declares; the bundle was
    first written with bare LF, which the eol suite catches.

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

commit d81d2a582fa97c5d4f8afc9a109514eb136311fa
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Sep 9 01:40:50 2026 +0530

    re-vendor LibKa0s v1.29.0 — the JSON dump folds into the report step

    `report` printed the summary and `dump` printed the run as one line of JSON.
    Both went to the same log, both described the same finished run, and
    perf-analysis asks for both -- so two verbs and two panel rows meant a second
    click, a second thing to remember, and a run reported without its dump was the
    easy mistake to make. `report` writes both now, JSON last, because the summary
    is what you read and the JSON is what you copy.

    `perf dump` is no longer a verb and `Progress().dump` is gone. Folded rather
    than aliased: an alias is the duplication the fold removes, and the unknown-verb
    path prints the usage, where `report` now says it renders the JSON too.

    Payload and kit copied whole from a clean checkout of the tag; the CLAUDE.md
    provenance line rolls in this commit. Kit revision unchanged at 15.

    diff -r against a clean v1.29.0 checkout: byte-empty, both payloads.
    869 passed, 0 failed, 0 skipped, 869 total
    Total: 0 warnings / 0 errors in 94 files

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

commit fca318fc00795ffab481d779704452a5b80467e4
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Sep 9 01:27:11 2026 +0530

    re-vendor LibKa0s v1.28.0 — the perf usage block renders correctly

    Perf.lua 9 only. The usage block printed `<start|measure|finish|canceleport|
    dump|showideoggle>`: the verbs are pipe-separated and the client reads `|r` as
    a color reset, `|h` as a hyperlink and `|t` as the end of a texture, so it ate
    all three and the words fused. The eaten `|r` also swallowed the reset closing
    the gold run, which is why the line stayed yellow. The pipes are doubled now.

    The block also hand-aligned a second column with spaces and wrapped each
    description onto a continuation line, which a proportional chat font cannot
    honor. It is one row per verb through the collection's own FormatRow now, the
    same formatter the slash-command help uses.

    Payload and kit copied whole from a clean checkout of the tag, not from the
    library's working tree, and the CLAUDE.md provenance line rolls in this commit.
    Kit revision is unchanged at 15. No member added, removed or renamed, so nothing
    in this addon had to adopt anything.

    diff -r against a clean v1.28.0 checkout: byte-empty, both payloads.
    869 passed, 0 failed, 0 skipped, 869 total

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

commit c79d351a714a300fe3d8cd01932c3cabcc92fbc8
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Sep 9 00:35:51 2026 +0530

    fix(cooldowns): attribute the GCD per spell, not per log line (#15)

    The first cut marked "was a GCD running when this line was written?" when the
    question is "is this line the GCD's doing?", and a second live trace showed the
    difference inside a minute.

    Two failures, opposite directions. A GCD flips a spell active NOW and back to
    ready ~1.5s LATER, by which point the GCD has ended -- so every closing half of
    the churn came out unmarked and half the noise stayed unlabeled:

      00:30:16 | 4/5 changed: active=[207167,108199,221562,49576] (gcd)
      00:30:18 | 4/5 changed: ready=[207167,108199,221562,49576]

    And worse, in the other direction:

      00:30:35 | 1/5 changed: ready=[47528] (gcd)

    Mind Freeze coming off its own 15s cooldown -- the single most interesting line
    in the log -- labeled as noise because an unrelated GCD happened to be running
    at that instant. Mind Freeze is OFF the global cooldown, so nothing about that
    transition was the GCD's doing.

    Attribution is per spell now and carried across polls. A spell that goes down
    while a GCD runs is attributed to it; a spell still down after the GCD has
    ended has the attribution withdrawn, because that is a real cooldown wearing a
    GCD's clothes. A transition to ready is attributed to whatever put the spell
    DOWN, not to the flag that happens to be set when it comes back up. The line is
    marked only when EVERY logged transition is the GCD's -- one real transition
    disqualifies it, because "(gcd)" on a line carrying a real cooldown reads as
    "skip me" over the one thing worth reading.

    The withdrawal had to go in the not-changed branch, which is the part that is
    easy to miss: a spell parked on a real cooldown does not change from poll to
    poll, so that is the only place its attribution can ever be corrected. Without
    it the mixed case stays marked and the fix looks right in three cases out of
    four.

    Written in place on the record already held -- no allocation -- and the whole
    attribution path is inside `if dbg`, so nothing runs when the console is not
    listening. lizard still reports 0 warnings, so M4-25's release gate holds.

    Three cases, all watched red against the first cut rather than against the
    original code: the closing half of a GCD, a real cooldown coinciding with one,
    and a line mixing both.

    869 passed / 0 failed (was 866), luacheck 0/0 in 94 files.

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

commit e95f204397380388affea04f4ee7b35b6e420828
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Sep 9 00:28:29 2026 +0530

    fix(cooldowns): mark the debug lines the global cooldown explains (#15)

    Found running smoke session 1 step 1.7 on live Retail. Seventeen [Cooldowns]
    lines in a 45-second fight, and two of them were cooldowns:

      18:08 | 5/5 changed: ready=[207167,108199,221562,49576] active=[47528]
      18:23 | 1/5 changed: ready=[47528]

    Mind Freeze on and off its 15s cooldown, exactly right. The other fifteen are
    the global cooldown -- four utility spells flipping active <-> ready in lockstep
    on every press, twice inside the same second at 18:13 and again at 18:18.

    buildSpellState derives both fields from the legacy active flag, and the header
    at :50 says what that flag covers: "real CD or just GCD". MaterialChange keys on
    exactly those two fields, so a GCD is material by its own test and gets a line.

    MARKED, NOT SUPPRESSED, and that is forced rather than preferred. The C-side
    curve evaluation that separates a GCD from a real cooldown cannot hand its
    answer back into a Lua `if`, and every duration involved is secret in combat --
    the same wall the header already documents for the icon path. Worse, any
    time-based heuristic that hid the churn would hide a real cooldown starting too:
    the 5/5 line above, the single most interesting line in the log, IS a GCD line.
    Spell 61304's plain-bool active flag is the one value in reach that can be
    branched on at all, so the line reports which it was and the reader judges.

    Scope stated honestly, because the issue overstated it first: this is not the
    flood MaterialChange was written to stop. That was "~10 identical lines per
    second"; this is 0.33/s and the lines are not identical. Nothing was broken and
    nothing was hidden by volume. What it cost was signal -- in the one situation
    the console exists for, fifteen of seventeen lines said nothing about whether an
    interrupt was available.

    The mock's GetSpellCooldown ignored its spellID and answered one fixed table for
    every spell, so the GCD could not be expressed at all -- which is why no case
    could have caught this. It is per-spell now, seeded through mocks.spellCooldowns,
    with unseeded ids answering the old inert shape so no existing suite moves.

    Two cases: a flip the GCD explains carries the marker, and one it does not carry
    none -- a marker on every line carries as much information as none at all. The
    first was watched red.

    Rendering is untouched: IconGrid still separates GCD from real cooldown C-side
    against a curve, which is the only place that distinction can be made correctly.

    866 passed / 0 failed (was 864), luacheck 0/0 in 94 files, lizard 0 warnings.

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

commit 06ec1755d76c47c57f8d325613e5bd933896a6f5
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 22:18:20 2026 +0530

    M5-06-C1: the lint citations name a section that does not exist

    The 2026-09-08 standards audit found these, and this cycle wrote them. M4c-03,
    M4c-04 and M4c-06 removed the blanket luacheck ignores across the collection and
    cited the rule as `lint-§1` in the configs, the gate cases and the testing docs.

    standards/lint.md carries exactly one heading and no numbered subsections, so
    `lint-§1` resolves to nothing. The house rule for a section-less file is the bare
    filename -- the same ruling M1-STD-04 made for standalone-windows.md, whose
    line-number citations exist for this reason.

    Seventy citations across nine repositories, all authored 2026-09-08, all now
    reading `lint.md`. No behaviour changes and no count moves: every hit is a
    comment or an assertion message.

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

commit 2095532bd4b7a51bd39b8b319bcab33f467a20ee
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 21:46:18 2026 +0530

    M5-06: the fresh audit against v2.39.0, and three findings only it can see

    A read-only compliance measurement of KickCD at 03f3b9a against the Ka0s WoW
    Addon Standard v2.39.0 (2026-09-07), written to docs/audits/2026-09-08/. No
    addon code, TOC, config or document outside that folder was changed, and
    docs/audits/2026-09-07/ was not touched.

    The standard was resolved before anything was measured: AUDIT.md,
    standards/STANDARDS.md (line 1 read v2.39.0) and all 26 section files the
    Sections list links, fetched with curl and read verbatim. Each was diffed
    against the 2026-09-07 copy so the run knows which rules moved -- fifteen
    sections changed, and three of this bundle's four new findings exist only
    against the amended text.

    Eight roots, no dependents. High 0, Medium 0, Low 5, Info 3; four MUST
    failures, every one of them Low because no user, no KickCDDB and no session
    can reach any of them today. The 2026-09-07 run filed eleven, of which nine
    are closed.

    The two that this cycle was supposed to close and did not:

      - KICKCD-A-08. M4-20 fixed the single site the audit cited
        (modules/Castbar_Debug.lua) and nothing swept for the class. Three
        `or _G.print` fallback arms survive at core/KickCD.lua:108,
        core/Compat.lua:452 and modules/Cooldowns.lua:538. The audit
        under-scoped and the fix followed the audit.

      - KICKCD-A-02. The MUST half closed -- five load-bearing TOC positions,
        five annotations naming what resolves. The SHOULD half did not:
        # Locales, # Defaults, # Modules and the # Core remainder still carry no
        conventional note, which M4-12's own commit message says was deliberate.

    The three that only the amended rules make visible:

      - KICKCD-B-01. documentation-§3 now specifies ### Verification and record
        and fixes its membership at exactly six docs, with perf-analysis/README.md
        registering in ### Conditional instead. KickCD's fourth table has seven
        rows. The table had no specification at all before this cycle.

      - KICKCD-B-02. localization-§5 now publishes the canonical BRITISH/ALLOWED
        lists and specifies the four exclusions as *frozen dated bundles*.
        tests/test_spelling.lua carries both lists whole -- 91 and 30, matching
        the published block entry for entry -- and then excludes the whole
        docs/perf-analysis/ directory, including the live README.md that
        documentation-§3 calls "the one file in the store that is rewritten". Two
        British spellings sit there un-caught, at :32 and :33.

      - KICKCD-B-03. audit-review-history's third MUST -- evaluate every register
        row's re-check trigger against the tree -- is new this cycle. It finds
        that docs/ARCHITECTURE.md:248 says "Two profile migrators run off the
        stored shape" while core/Database.lua:598-601 runs three, and has done
        since e143516 on 2026-07-16, the row's own Decided date. Three audits
        accepted the row without re-reading the count.

    Measured, not reasoned: luacheck 0/0 over 94 files with the test tree in
    scope; 864 cases green; lizard's verbatim invocation reports zero warnings
    against the record's zero, so the release gate KICKCD-R-02 blocked is clear;
    the line-ending working-tree sweep returns 0 against the previous run's 8; and
    both diff -r gates against LibKa0s v1.27.0 -- the ship folder and the test kit
    -- are empty. Every file:line in the bundle was re-read and quoted beside its
    citation, and every count carries the command and the scope that produced it.

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

commit 86744a46c804dd62e04ef75873ee3c401faf7c1b
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 20:14:34 2026 +0530

    M4c-06: the blanket ignore goes, and thirty-two of the sixty-one were real

    `.luacheckrc:12-16` carried `ignore = { "212/self", "212/event", "211/addonName" }`.
    This one is worth naming precisely, because it does not look like the blanket the
    other two repos carried. Every entry was already in luacheck's narrow
    `<code>/<variable>` form. That is the narrow SPELLING, and it sat at the top
    level, which is the widest SCOPE there is: those three names were silenced in all
    93 files, including every file with no business producing them. `M4-11` is about
    scope, not spelling -- an ignore that silences the wall reads as coverage and
    provides none.

    It provided none here, and the measurement is in the tree. Removing the three
    lines took `luacheck .` from 0/0 to **61 warnings** -- 29 `211/addonName` and 32
    `212/self`, no `542` anywhere -- and **thirty-two of the sixty-one were not
    conventions at all**:

      * Twenty-nine files opened `local addonName, NS = ...` over a folder name they
        never read. Five files in this addon do read it -- CoreSetup, EnvSetup,
        MediaSetup, DebugLogSetup and PerfSetup, each handing it to a vendored LibKa0s
        payload that cannot infer which folder it was copied into. The other
        twenty-nine had the line because it was copied. They now open
        `local _, NS = ...`, which is how core/PoolSetup.lua already spelt it.

      * Two receivers in the test tree were named and never read: the mock module
        method in tests/test_util.lua and `t.SendMessage` in tests/wow_mock.lua. Both
        are `_` now, which keeps the arity the mocked signature owes while dropping
        the claim that a sender is consulted.

      * One dead function. settings/Slash.lua carried
        `function NS.Slash:PrintHelp() return NS.Slash.cli:PrintHelp() end` and
        nothing called it: core/KickCD.lua:253 reaches `NS.Slash.cli:PrintHelp()`
        directly, behind the same "did settings/ load at all" guard the forwarder
        carried, and `NS` is private (there is no `_G.KickCD`), so nothing outside
        could reach it either. The blanket is what hid it -- with the receiver
        unreported the line read like the third member of a trio beside `LandingRows`
        and `OnSlash`, both of which are called. Deleted, with a comment in its place
        saying why there are two forwarders and not three.

    `212/event` deserves its own line: it matched **nothing**. No file in this tree
    produces an unused `event`. It was carried for a convention that is not here, and
    while a suppression is switched on everywhere nobody can tell a live one from a
    stale one. That is the second cost of a blanket, after the one the rule names.

    Twenty-nine remain, all `212/self`, answered by nine per-file `files[...]`
    stanzas: the two version-gated Database migrators reached through the
    `migrations` scaffold, `NS:OnEnable`, the four AceAddon module objects whose
    methods every caller reaches with the colon, the text-icon registry pair in
    IconGrid_Render, the `LibKa0s-Slash-1.0` degradation stub and the two `NS.Slash`
    forwarders, and the spells page. Each carries a comment naming the obligation
    that forces the receiver. `IconGrid:OnSpellState` is the sharpest of them: it is
    registered BY NAME at modules/IconGrid.lua:769, and AceEvent-3.0 invokes a
    name-registered handler as `self[method](self, ...)`, so the receiver is not this
    addon's choice at all.

    The narrowing was measured, and measured for the right thing, which took two
    tries. A dead argument under a NEW name always reported here -- the old entries
    named their variables, so adding a `deadArg` to `Castbar:GetCastbarFrame` goes
    red under both configs and proves nothing. The claim that quoted it was written
    into this file's comments and then removed, because it was false. What the old
    config actually swallowed is the NAME it listed, in every file: appending
    `function NS.Util:DeadProbe() return 1 end` to core/Util.lua reports
    `core/Util.lua:429:17: (W212) unused argument 'self'` under the new config and
    reports nothing under the old one, re-linted with `luacheck --config` against the
    blanket. The twenty-nine `addonName` headers are the same fact at scale -- they
    sat under a green 0/0 for the life of the addon.

    The gate is the other half, and `M4c-03` and `M4c-04` are the shape it follows: a
    top-level `ignore` is one line for anyone to re-add. tests/test_lintconfig.lua
    loads `.luacheckrc` as Lua under a sandbox that auto-creates tables the way
    luacheck's own config loader does, so it inspects the table luacheck obeys rather
    than text a different spelling would slip past. Four cases, and all four were
    watched red in the working tree before this commit: a top-level `ignore`; a class
    switched off wholesale (`unused_args = false` and eight relatives); a `files[...]`
    ignore whose key is a directory and whose entry names no variable; and a bare
    `-- luacheck: ignore` anywhere in tracked Lua. It fails rather than skips when it
    cannot look, the bargain test_doc_structure and test_eol already strike here.

    Documentation moved with the code. docs/common-tasks.md taught the retired
    convention in the most direct way possible -- it described the three entries as
    "allowances true everywhere" and closed with "prefer the inline form" -- and now
    says there is no top-level ignore, what removing it exposed, and that fixing the
    code beats either suppression. docs/ARCHITECTURE.md said "every source file opens
    with `local addonName, NS = ...`" over a code block spelling it that way, and now
    names the five that read the folder name and the thirty that write `_`. CLAUDE.md
    carried the same header inline. docs/testing.md gains the gate as the sixth of its
    source-scan guards. DEPENDENCIES.md's `.luacheckrc:10-43` citation for the
    `read_globals` block was stale before this change and is now `:47-80`.

    Verification, observed. `luacheck .` 0 warnings / 0 errors in 94 files.
    `lua tests/run.lua` 864 passed, 0 failed, 0 skipped -- 860 before, plus the gate's
    four. docs/test-cases.md regenerated through `--list` and `diff` against it is
    empty; its Totals row reads 864 and the README `[tests]` badge moves 860 to 864 in
    this commit. No version anywhere: Decision 5 stands and the badge is a count.

    No smoke step, and none is claimed. Thirty shipped files changed and every change
    in them is inert: twenty-nine rename a local nothing read, and the thirtieth
    deletes a member nothing called. Lua does not care what an unread local is named,
    and a table field with no reader has no observable removal. Nothing else in the
    change reaches the client -- `.luacheckrc` and tests/test_lintconfig.lua are not
    packaged, and the rest is documentation.

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

commit 86744a46c804dd62e04ef75873ee3c401faf7c1b
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 20:14:34 2026 +0530

    M4c-06: the blanket ignore goes, and thirty-two of the sixty-one were real

    `.luacheckrc:12-16` carried `ignore = { "212/self", "212/event", "211/addonName" }`.
    This one is worth naming precisely, because it does not look like the blanket the
    other two repos carried. Every entry was already in luacheck's narrow
    `<code>/<variable>` form. That is the narrow SPELLING, and it sat at the top
    level, which is the widest SCOPE there is: those three names were silenced in all
    93 files, including every file with no business producing them. `M4-11` is about
    scope, not spelling -- an ignore that silences the wall reads as coverage and
    provides none.

    It provided none here, and the measurement is in the tree. Removing the three
    lines took `luacheck .` from 0/0 to **61 warnings** -- 29 `211/addonName` and 32
    `212/self`, no `542` anywhere -- and **thirty-two of the sixty-one were not
    conventions at all**:

      * Twenty-nine files opened `local addonName, NS = ...` over a folder name they
        never read. Five files in this addon do read it -- CoreSetup, EnvSetup,
        MediaSetup, DebugLogSetup and PerfSetup, each handing it to a vendored LibKa0s
        payload that cannot infer which folder it was copied into. The other
        twenty-nine had the line because it was copied. They now open
        `local _, NS = ...`, which is how core/PoolSetup.lua already spelt it.

      * Two receivers in the test tree were named and never read: the mock module
        method in tests/test_util.lua and `t.SendMessage` in tests/wow_mock.lua. Both
        are `_` now, which keeps the arity the mocked signature owes while dropping
        the claim that a sender is consulted.

      * One dead function. settings/Slash.lua carried
        `function NS.Slash:PrintHelp() return NS.Slash.cli:PrintHelp() end` and
        nothing called it: core/KickCD.lua:253 reaches `NS.Slash.cli:PrintHelp()`
        directly, behind the same "did settings/ load at all" guard the forwarder
        carried, and `NS` is private (there is no `_G.KickCD`), so nothing outside
        could reach it either. The blanket is what hid it -- with the receiver
        unreported the line read like the third member of a trio beside `LandingRows`
        and `OnSlash`, both of which are called. Deleted, with a comment in its place
        saying why there are two forwarders and not three.

    `212/event` deserves its own line: it matched **nothing**. No file in this tree
    produces an unused `event`. It was carried for a convention that is not here, and
    while a suppression is switched on everywhere nobody can tell a live one from a
    stale one. That is the second cost of a blanket, after the one the rule names.

    Twenty-nine remain, all `212/self`, answered by nine per-file `files[...]`
    stanzas: the two version-gated Database migrators reached through the
    `migrations` scaffold, `NS:OnEnable`, the four AceAddon module objects whose
    methods every caller reaches with the colon, the text-icon registry pair in
    IconGrid_Render, the `LibKa0s-Slash-1.0` degradation stub and the two `NS.Slash`
    forwarders, and the spells page. Each carries a comment naming the obligation
    that forces the receiver. `IconGrid:OnSpellState` is the sharpest of them: it is
    registered BY NAME at modules/IconGrid.lua:769, and AceEvent-3.0 invokes a
    name-registered handler as `self[method](self, ...)`, so the receiver is not this
    addon's choice at all.

    The narrowing was measured, and measured for the right thing, which took two
    tries. A dead argument under a NEW name always reported here -- the old entries
    named their variables, so adding a `deadArg` to `Castbar:GetCastbarFrame` goes
    red under both configs and proves nothing. The claim that quoted it was written
    into this file's comments and then removed, because it was false. What the old
    config actually swallowed is the NAME it listed, in every file: appending
    `function NS.Util:DeadProbe() return 1 end` to core/Util.lua reports
    `core/Util.lua:429:17: (W212) unused argument 'self'` under the new config and
    reports nothing under the old one, re-linted with `luacheck --config` against the
    blanket. The twenty-nine `addonName` headers are the same fact at scale -- they
    sat under a green 0/0 for the life of the addon.

    The gate is the other half, and `M4c-03` and `M4c-04` are the shape it follows: a
    top-level `ignore` is one line for anyone to re-add. tests/test_lintconfig.lua
    loads `.luacheckrc` as Lua under a sandbox that auto-creates tables the way
    luacheck's own config loader does, so it inspects the table luacheck obeys rather
    than text a different spelling would slip past. Four cases, and all four were
    watched red in the working tree before this commit: a top-level `ignore`; a class
    switched off wholesale (`unused_args = false` and eight relatives); a `files[...]`
    ignore whose key is a directory and whose entry names no variable; and a bare
    `-- luacheck: ignore` anywhere in tracked Lua. It fails rather than skips when it
    cannot look, the bargain test_doc_structure and test_eol already strike here.

    Documentation moved with the code. docs/common-tasks.md taught the retired
    convention in the most direct way possible -- it described the three entries as
    "allowances true everywhere" and closed with "prefer the inline form" -- and now
    says there is no top-level ignore, what removing it exposed, and that fixing the
    code beats either suppression. docs/ARCHITECTURE.md said "every source file opens
    with `local addonName, NS = ...`" over a code block spelling it that way, and now
    names the five that read the folder name and the thirty that write `_`. CLAUDE.md
    carried the same header inline. docs/testing.md gains the gate as the sixth of its
    source-scan guards. DEPENDENCIES.md's `.luacheckrc:10-43` citation for the
    `read_globals` block was stale before this change and is now `:47-80`.

    Verification, observed. `luacheck .` 0 warnings / 0 errors in 94 files.
    `lua tests/run.lua` 864 passed, 0 failed, 0 skipped -- 860 before, plus the gate's
    four. docs/test-cases.md regenerated through `--list` and `diff` against it is
    empty; its Totals row reads 864 and the README `[tests]` badge moves 860 to 864 in
    this commit. No version anywhere: Decision 5 stands and the badge is a count.

    No smoke step, and none is claimed. Thirty shipped files changed and every change
    in them is inert: twenty-nine rename a local nothing read, and the thirtieth
    deletes a member nothing called. Lua does not care what an unread local is named,
    and a table field with no reader has no observable removal. Nothing else in the
    change reaches the client -- `.luacheckrc` and tests/test_lintconfig.lua are not
    packaged, and the rest is documentation.

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

commit 8d601c020c78da9bca94e608414452ad1b3ce480
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 19:13:42 2026 +0530

    M5-03: the hub gets its two mandated names, behind a gate over all ten

    KickCD was the only one of the nine whose docs/ARCHITECTURE.md did not carry
    `## Overview` and `## Module map`. Nothing was missing — the orientation prose
    sat under "What it does" and the subsystem table under "Subsystems at a glance"
    — but a reader arriving from AbsorbTracker or LootHistory looks for the mandated
    names, does not find them, and cannot tell from the page whether that is a
    ratified deviation or an oversight. `documentation-§3` names the sections; this
    renames them and leaves `## Load order` where it is, as the extra section it has
    always been.

    The rename comes with a lead-in under `## Module map` saying what the section
    holds and where the rest is: per-file responsibilities in module-map.md, and the
    TOC order in `## Load order` at the foot of the page. That last one is an anchor
    into this file, which is the half of a heading rename that quietly breaks. A dead
    `#fragment` raises nothing — GitHub scrolls to the top and the reader assumes
    they misread — so tests/test_doc_structure.lua checks both halves: the mandated
    headings exist, and every `](…ARCHITECTURE.md#frag)` in the tracked markdown
    outside libs/, tests/_kit/ and the frozen dated bundles lands on a heading that
    does. The two anchors already pointing here, both to `#documented-deviations`
    and `#invariants-worth-not-breaking`, survive the rename untouched; the gate is
    what makes that a fact rather than a hope.

    The gate names all ten sections documentation-§3 mandates, not only the two that
    moved — Overview, Module Map, Settings Schema, Message Bus, Slash Commands,
    Event Subscriptions, Taint Notes, Known Limitations, `## Documentation map` and
    `## Documented deviations`. Naming them beats counting them, because a bare count
    goes stale silently, and a gate holding two of them lets the other eight be
    renamed by the same reflex that gave this repository "What it does" in the first
    place. All ten are present and were before the rename, so the wider gate watches
    more without changing a line of prose. The pattern is built per name rather than
    typed out, case-insensitive on every letter, because the collection is split
    between `## Module map` and `## Module Map` and the section names a section, not
    a capitalization. A failure lists every missing heading at once instead of
    stopping at the first.

    Watched it red before it was green: with the old headings in the tree the first
    case fails on the `## Overview` pattern and names the section it could not find.

    Two cases, so 857 -> 859 — docs/test-cases.md regenerated through
    `lua tests/run.lua --list` and the README badge moved in this commit. Verified
    from a clean clone of this tree, not the working tree: luacheck 0 warnings /
    0 errors in 93 files, and 859 passed, 0 failed, 0 skipped, 859 total.

    This is two commits squashed into one, and why is worth recording. The first
    renamed the sections and added the gate, and its message reported "luacheck 0/0,
    859 passed, 0 failed" — a figure it never observed. That tree replays 858 passed,
    1 failed, 859 total from a clean clone: the spelling gate catches "normalised"
    in the new test file's own comment and fails "authored English is US English"
    (localization-§5). The second commit widened the gate to ten sections and fixed
    that spelling on the way through. So the first commit sat red on the branch,
    against the invariant that no commit leaves the repository red, and it reported
    itself green. Folding the fix into it clears the red. Saying so here is the part
    that matters more, because a count that was written instead of measured is worse
    than a check nobody ran — every report downstream of it inherited a number with
    no run behind it.

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

commit 7b1e1a13634bc79ad3122852e0c4bf39b40f977c
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 18:22:00 2026 +0530

    M5-01: the record is regenerated, and the zero that unblocks a tag is measured

    `suites.complexity.warnings` reads 0 here, and it is not the same zero the last run recorded.
    Between the two, `lizard` warned on the anonymous case at `tests/test_schema.lua:595-631` — 29
    NLOC at CCN 18 — and `automated-tests-§3` refuses a tag while that figure is non-zero. One test
    case that had grown a second half was the only thing standing between this addon and a passable
    release gate. `M4-25` split it on the seam its own comment named. This run is the measurement
    that says the split worked.

    Everything in `RESULTS.md` is the runner's now: the lead-in, the four standing suite sections
    and both watch-list tables, from this run's own manifest and `lizard` output. Until test-kit
    revision 15 the two tables `automated-tests-§4` mandates had no producer, which is why this
    file's watch list said what it said.

    And what it said was wrong. All four band files carried a disposition reading "unchanged", and
    three of them had moved: `settings/Spells.lua` 1171 → 1312, more than the other three together;
    `modules/Castbar.lua` 1305 → 1345; `tests/wow_mock.lua` 1128 → 1232. Only `modules/IconGrid.lua`
    held, one line down at 1152. Every cell now says where its file actually is, and Spells gets a
    re-check at 1400 rather than at the cap, because the in-file peel there is spent and the next
    reduction has to be a file split.

    Green on all four. Lint 0/0 over 93 files — 35 before `M4-11` put the test tree in scope, so
    `tests/wow_mock.lua` is linted now and was not — 860 cases passed with none failed and none
    skipped, six perf scenarios, zero above CCN 15 with five functions sitting at exactly 15.

    The perf suite gained `castStart` and lost close to half its allocation on two scenarios:
    `spellPoll` 978.1 → 526.3 bytes/iter and `spellState` 362.9 → 210.6, with api/iter unchanged in
    both. The milliseconds all rose a sixth and are not comparable across runs, so they say nothing.

    The regeneration dropped one hand-written section, *A note on this record's own line endings*.
    Its substance is in the bundle's ANALYSIS.md, and it has stopped needing a maintainer: two cases
    in the suite now assert on every run what that paragraph asserted once by hand. A paragraph
    became a gate.

    `20260807-110522` and `20260825-103417` still have no ANALYSIS.md and are not getting one.
    Dating a reading to a day nobody took it is worse than a gap. This bundle has one.

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

commit 6f4b6696f2310ba56938ad726539c7b375aa056a
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 17:44:37 2026 +0530

    M5-02: the provisional row gets its ruling, and a dead ledger marker goes

    This repository wrote the one register row in the collection that said of itself
    it was not ratified. It measured a library-less load registering 112 of 228 schema
    rows, named the two `options-ui-§1` MUSTs it sat between, and asked the standard
    which of them wins when the missing content is composed rather than merely
    rendered. That was the right thing to do — the alternative was eight more addons
    each deciding a question that was never theirs — and it has an answer now.
    `options-ui-§1` rules that the no-copy MUST wins, that a stub's composer members
    exist and answer an empty row list, and, in as many words, that "this shape needs
    no register row, and the rows already written for it retire". So the row retires,
    and the paragraph beneath the table becomes the record of the question and its
    answer rather than of a decision still owed.

    Nothing about the code changes and nothing was supposed to. The three bounds the
    ruling sets are the three this row had already measured: the fall-together
    property, profile defaults coming off `defaults/Profile.lua` rather than off the
    schema, and a suite pinning both counts plus the delta —
    `tests/test_options_panel.lua:411-429` fingerprints composed rows by their `order`
    field and has since it was written. `settings/OptionsSetup.lua`'s comment named
    the row as the deviation this pass most wanted reviewed; it now names the ruling,
    because a comment pointing at a row that no longer exists is the next reader's
    wasted hour.

    `KickCD.toc:14` carried `# ## X-Wago-ID: <id>   -- TODO(KCD-18): add once
    published on Wago`. `KCD-18` is a row of `docs/pending/LEDGER.md`, which is gone
    collection-wide, so the marker pointed at a store that does not exist and nothing
    would ever have re-surfaced it. `toc-file-§1` makes a distribution id mandatory
    only for a platform the addon actually ships on and this addon is CurseForge-only,
    so there is no work here to track: the marker goes and the comment says plainly
    why the field is absent and what would bring it back. `KICKCD-A-11`.

    The gate goes into `tests/test_doc_structure.lua`, beside the other checks on this
    file's shape. It slices `## Documented deviations`, pulls every `KCD-` or
    `KICKCD-`-shaped id out of it and requires each to be ASSIGNED by a bundle under
    `docs/audits/` — heading a table cell, a heading or a bullet, not merely quoted in
    prose, because a bundle reporting a dead citation quotes the dead id while doing
    so. It borrows this suite's `read` and `fail`, so a missing `docs/audits/` is a
    failure rather than a silent pass. Watched go red by planting `KICKCD-A-99` in the
    retirement note; it named that id and nothing else.

    luacheck 0/0; 860 passed, 0 failed. The count moved by one, so `docs/test-cases.md`
    and the README `[tests]` badge moved in this commit. No version, no TOC version,
    no CHANGELOG. `libs/` and `tests/_kit/` untouched.

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

commit 836711aeba3e15dc123eea56a536c66c2f649144
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 16:56:59 2026 +0530

    M4c-02: the spelling sweep gets a gate, because the sweep did not hold

    `M4-13` swept this tree against localization-§5's published list and left it clean. That is
    verifiable rather than asserted: `git archive 6eddbd4` through the section's own algorithm
    returns exactly one line, the deliberate quote waived below. Eight commits later the same
    scan returns seventeen.

    Nothing regressed it on purpose. Five separate items put the spellings back, none of them
    about spelling and none of them able to see what they had done. `M4-12` wrote "generalise"
    into the TOC comment that explains why `settings/OptionsSetup.lua` sits where it does.
    `M4-17` wrote "behaviourally" into `tests/test_source_style.lua`'s own header. `M4-18` wrote
    "screen centre" twice, once in `docs/smoke-tests.md` and once in `tests/test_settings_log.lua`,
    about the same coordinate. `M4-20` wrote "in favour of" and "behaviour" into the smoke doc's
    new section 26. And `M4-21` transplanted the locale lexer PrettyChat and ConsumableMaster had
    landed, whose residue taxonomy names a class `SPLIT COLOUR` -- eleven of the seventeen lines
    are that one word, carried in the register's comment block, in eight register entries and in
    the class table that validates them.

    That last one is the answer to "fix the source of the copy, not just the symptom", and the
    answer is uncomfortable: the source is two OTHER repositories. `PrettyChat/tests/test_locale.lua`
    and `ConsumableMaster/tests/test_locale.lua` both spell the class the British way today, and
    both hold uncommitted work in other lanes right now. This commit does not reach into either.
    What it does instead is make the copy's arrival visible HERE, which is the only half of the
    repair that lives in this repository: the next transplant carrying a British spelling reddens
    `lua tests/run.lua` in the working tree before it can be committed.

    So `tests/test_spelling.lua`. It carries localization-§5's `BRITISH` and `ALLOWED` lists whole
    and adds nothing, because the section says to and because a private subset is a gate whose
    green tells a reader nothing about which spellings it covers -- PanelMaster's older gate,
    written before the list was published, is a 90-entry list of its own and cannot be compared
    with anything. `ALLOWED` is removed as whole words before the substring pass, which is the half
    easiest to get wrong in both directions: matched as a substring it would swallow "analysed"
    inside the allowance for "analyses", and skipped entirely it would fail the build on
    "optimistic", "specialist" and "organism". Nineteen literal US words and fifteen literal
    British ones pin both directions, so a matcher narrowed back to nothing still reports red.

    The candidate set is `git ls-files`, for the reason `tests/_kit/test_eol.lua` gives about its
    own: a hand-typed file list is a list the next document quietly falls out of, and this repo has
    thirty-one live Markdown files no suite would otherwise open. `.superpowers/` is gitignored and
    carries four British spellings that belong to a tool rather than to us; it is not scanned
    because it is not tracked, which is the right reason rather than a coincidence. The four
    exclusions localization-§5 requires are named directory by directory and file by file --
    `libs/` and `tests/_kit/` as vendored, the five frozen `docs/` bundle roots as record, and this
    file as the document that spells the forbidden forms in order to forbid them.

    One waiver, and it is per WORD rather than per file. `docs/smoke-tests.md`'s session-3 check
    quotes `CANCELLED` and `unlabelled` in order to tell the reader that a double L in the client
    means the string did not come from the LibKa0s payload; correcting the quote deletes the check.
    `M4-13` made that call in prose. A whole-file waiver would have honored it and simultaneously
    hidden two of the five regressions above, both of which were in that same file, so the waiver
    names the two words and the document stays under the scan for everything else. Two assertions
    pin that boundary in both directions.

    Watched red in the working tree first. The gate went in alone and named all seventeen lines by
    file, line and word; the seventeen were then corrected and it went green. Both halves are in
    this commit because the invariant says so, and because a gate committed green proves nothing.

    The plan's acceptance grep now returns three lines rather than none, all in
    `docs/perf-analysis/` and `docs/revendor/` -- the same two frozen roots `M4-13` reported and
    03_SPEC.md names as a non-goal -- plus this file's own copy of the lists. With
    `--exclude-dir=perf-analysis --exclude-dir=revendor --exclude=test_spelling.lua` it returns
    nothing, verified against a clean `git archive` of this commit rather than the working tree.

    854 -> 857 cases; `docs/test-cases.md` and the README badge move here.
    `docs/common-tasks.md` gains the house rule and `docs/testing.md` records the gate as the fifth
    of its source-scanning family, since a rule enforced by a suite and written down nowhere is how
    this repository got here.

    No smoke step. Every change outside the suite is a comment or a document; the one shipped file
    touched is `KickCD.toc`, and the line is a comment about load order. Nothing a client renders
    moves.

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

commit 056031cbcd567866550a4f412fc3bd948b111363
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 16:44:22 2026 +0530

    M4c-01: the locale lexer was three answers in one loop

    M4-25 took this repo to zero complexity warnings, and it did that for one
    reason: automated-tests.md:143 gates a tag on suites.complexity.warnings == 0,
    so a single warning anywhere is a release KickCD cannot cut. M4-21 then
    transplanted a locale lexer into tests/test_locale.lua and put the number back
    to one. lizard -l lua -x './libs/*' -x './tests/_kit/*' . measured
    scanLiterals@227-277 at CCN 21 against a threshold of 15 -- 49 NLOC, one warning
    in a repo of 2254 functions. The gate was red in the working tree before this
    commit, which is where I watched it.

    The seam is the function's own header. It says the lexer is "reduced to the two
    questions asked here: where does each string literal start, and is it inside an
    `L[…]` subscript" -- two questions, and the loop answered both at once, in the
    same fourteen-branch dispatch, with a third job (finding the end of a quoted
    run) inlined in the middle of it. One of the two answers had already been
    hoisted out once: longBracket exists because a long-bracket lexeme's end is its
    own question. Its short-quote sibling had not been.

    So: endOfQuoted comes out beside longBracket, answering the same question for
    `'` and `"`, and trackSubscript comes out to own the depth counter -- the "am I
    inside an `L[…]`" half, whole-word `L` test and all. scanLiterals keeps the
    dispatch and nothing else, and the dispatch is what its remaining comment now
    explains, because the ORDER is the contract: newline first so the line counter
    is never wrong, then a comment, then the two literal forms, and only what none
    of them claimed reaches the tracker. That ordering is why a `"` inside an `L[…]`
    is recorded as a wrapped literal rather than eaten by the depth counter, and it
    is preserved exactly. CCN 21 becomes 11, 7 and 5.

    The prose split with the code, as it did in M4-25. The SUBSCRIPT DEPTH paragraph
    -- why `wrapped` counts brackets instead of reading the characters in front of
    the quote, and why `NS.L["…"]` counts while `SPELL_KNOWN_LABEL[field]` does not
    -- moves onto trackSubscript, which is now the only code it describes. The
    long-bracket paragraph stays with longBracket. What is left at the top is the
    map: three helpers and a dispatch.

    No behaviour moved, and I did not take that on trust. Running the HEAD scanner
    and this one over all 152 .lua files in the repo, 13232 literals came back
    identical in text, line and wrapped, with zero mismatches. The gate still bites:
    adding `local M4C01_PROBE = "This sentence is deliberately not routed through L"`
    to settings/Spells.lua fails "every user-facing literal in the settings surface
    is routed or recorded" and only that case. The probe is reverted; the working
    tree carries only the split.

    No count moved -- this splits a helper, not a test case, so 854 stays 854 and
    docs/test-cases.md and the README [tests] badge are untouched by design. No
    version anywhere: this cycle does not ship. No smoke step, and no smoke step is
    owed: a test file's internals reach no client-visible surface.

    Verified in KickCD: luacheck . 0 warnings / 0 errors in 91 files;
    lua tests/run.lua 854 passed, 0 failed, 0 skipped;
    lizard -l lua -x './libs/*' -x './tests/_kit/*' . -- 0 warnings, against 1
    before this commit.

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

commit 2c7a4f127608e8f280992f02b5dc63b7005d7248
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 15:54:18 2026 +0530

    M4-22: the cast bar minted a closure per cast, and nothing in the repo could see it

    KICKCD-R-10, which is this repository's whole share of the item. The two ceilings
    the plan names for re-baselining are AbsorbTracker's and MultiMeters'; the false
    mouseover comment is PanelMaster's; ConsumableMaster's timer burst landed in its
    own repo. What is here is the second of the three gated allocation nits, and the
    gate is the whole point of the item: the plan takes these "only with a scenario
    that measures them, added first — otherwise skip them". So tests/perf.lua's
    castStart scenario went in and was watched red before modules/Castbar.lua moved.

    THE PREMISE STILL HOLDS ninety-odd commits on. modules/Castbar.lua:833 is still
    `inst.frame:SetScript("OnUpdate", function() onUpdate(inst) end)` inside Start,
    still reached once per UNIT_SPELLCAST_START per tracked unit, and tests/perf.lua
    still had four scenarios, none of them a cast start. The review graded the code
    fact confirmed and the perf claim unverified, and it was right to: nothing in
    this repo could attribute a byte to that line, in or out of the client.

    THE LOAD-BEARING ASSERTION IS AN IDENTITY, NOT A NUMBER, and that is the
    difference between this scenario and a byte column somebody learns to ignore.
    "Cast start mints a fresh closure" is a property of the code: the same handler
    object or two different ones, the same answer on every interpreter, immune to
    what the collector happened to have reclaimed. So the scenario starts a cast
    twice and compares the installed handlers, and the byte ceiling sits behind it
    as the backstop for a shape that caches the handler and then allocates something
    else per cast — which the identity check alone would wave through.

    Red on both, three runs, identical: two distinct handlers, and 304.0 bytes per
    start/stop pair. Green on both, three runs, identical: one handler, 208.0 bytes.
    The 96-byte return is the closure and its upvalue box.

    THE CEILING IS 288 AND ITS WIDTH IS MEASURED, not guessed. One empty table added
    to the scenario's body moves the figure 208.0 -> 273.0, so a table costs 65 bytes
    per pass here; 288 therefore admits exactly one and rejects the defect's 304.0 by
    16 bytes. Wide rather than tight on purpose, because a byte figure taken this way
    reports what the collector has NOT reclaimed by the end of the loop and can move
    with unrelated edits to this file. The identity leads; this follows.

    TWO PROFILE WRITES SET UP THE SCENARIO AND BOTH ARE LOAD-BEARING. `locked` ships
    false, and an unlocked Stop calls ShowPreview — a full re-skin — so the shipped
    default would have timed the drag affordance instead of the cast. `visibility`
    ships "target_casting_interruptible", under which isVisible is false and Start
    returns before the SetScript line the scenario exists to measure. A player
    watching a cast bar in combat is locked and visible, and that is what is set.

    THE FORWARD DECLARATION IS THE PART A READER WOULD GET WRONG. onUpdate is a file
    local defined at :681, EnsureFrame is at :441, so EnsureFrame cannot name it
    without one. Hoisting onUpdate instead would drop a per-frame render function
    into the middle of the widget build and split it from RenderCast and ApplyState,
    which are one story; `local onUpdate` in the helpers block is the smaller move.
    The closure captures the local as an upvalue and EnsureFrame only ever runs after
    load, so the assignment below is always in place by then.

    EnsureFrame is the right home for it because it is the only code that runs once
    per unit for the life of the session — inst.frame is never cleared anywhere in
    this file, so its early return is the guard — and Start calls it before every
    install, so inst.onUpdateScript cannot be nil at the SetScript line. A cached
    handler also makes the install idempotent, which the old shape was not:
    SetScript with the same function twice is a no-op, with two equal-but-distinct
    closures it is not.

    PER-UNIT, NOT ONE FILE-SCOPE HANDLER, and the new case pins that too. The obvious
    way to kill this allocation outright is a single shared function, and a single
    function cannot carry the instance — focus would drive the target's bar. Every
    existing case in tests/test_castbar_frame.lua would stay green through that,
    because they drive one unit at a time. So the case asserts three things: the same
    handler across two starts on one unit, that it is the one EnsureFrame cached, and
    that target and focus get different ones. Watched red at "the second cast start
    installed a DIFFERENT handler", green after. It lives in the headless suite
    rather than only in tests/perf.lua because that runner is deliberately outside
    the green gate and nothing in CI would notice the property coming undone.

    853 -> 854, with docs/test-cases.md and the README badge moving here.

    THE LOCAL CEILING NEEDED NO RE-BASELINING and now says so with its date.
    PROBE_OFF_BYTES_CEILING is 900 against a measured 848.0, identical to the tenth
    across three runs today and unmoved by this commit's new scenario — 6.1%, not
    AbsorbTracker's 6.7x, so it is not one of the ceilings that bounds nothing. What
    its one-line comment did not carry was the size of what it catches, which is now
    measured: one empty table on that path costs 64 bytes/pass, so the 52-byte margin
    admits none of them and the smallest realistic regression trips it. Recorded in
    the same three-line shape the sibling repos used, so the next person re-deriving
    it has the figure, the margin and the date rather than an assertion.

    The `Smoke, session 3` step added at docs/smoke-tests.md § 32 has NOT been run —
    no WoW client was available. Folded into session 3 because that is this repo's
    outstanding run. Section 7a already watches one bar fill and snap off and would
    catch a handler that was never installed; what it cannot see, because it drives
    one unit, is a handler that IS installed and bound to the wrong instance. Nothing
    headless calls the handler at all — SetScript under the mock is a table write —
    so two bars animating independently through consecutive casts is a client-only
    observation.

    Covers KICKCD-R-10.

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

commit 990ead1d834c2b7d075888b086748c6b77b553c2
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 15:42:36 2026 +0530

    M4-21: the locale gate stops looking only where the strings already are

    KICKCD-R-03's premise still holds. `settings/Castbar.lua` renders three cast-bar
    `desc` sentences -- the two "Overridden by Auto-size to icon grid" clauses at
    :219 and :227 and the "Capped at the bar's short axis" one at :302 -- and
    `locales/enUS.lua` defined the SHORT forms those replaced. Nothing was visibly
    wrong because `locales/enUS.lua:15`'s `__index` returns the key, so an enUS
    client renders the long sentence perfectly; the only person who would ever have
    found out is the first translator, who would have been handed three sentences
    the panel does not show and no sign of the three it does. The three current
    sentences are defined now and the three superseded ones are gone.

    Gone rather than kept alongside, and that is the part worth a sentence. A
    manifest carrying both leaves a translator two strings and no way to tell which
    one is live. `locales/enUS.lua` holds 82 further keys that nothing subscripts --
    "Bar texture", "Master controls", the border block -- and those are NOT touched
    here: they are strings whose rows now come from a LibKa0s composer, which is a
    different defect with a different fix. These three are superseded IN PLACE, the
    row still exists and its wording moved, which is the case M4-21 names.

    The other half of the plan row is the coverage case, and its shape is the point.
    The obvious locale gate is a `gmatch` for `L["..."]` over the sources checked
    against the manifest. Four repositories in this collection had one and it is
    `testing-§12`'s failure mode written out: everything such a scan can find is by
    construction already wrapped, so the one thing it exists to catch is the one
    thing it cannot see. KickCD never had that case at all -- `tests/test_locale.lua`
    tested locale INDEPENDENCE (issue #8, the frFR spec-key bug) and asked nothing
    about routing -- which is how three undefined keys sat under 847 green cases.

    What is here instead lexes the TOC-derived source list for string LITERALS and
    asks two questions in opposite directions. Every literal that IS an `L[...]`
    subscript must be defined in `locales/enUS.lua`. Every literal in `settings/`
    that is NOT one and reads as prose must be recorded, with a declared class, in
    the residue register at the foot of the file -- and an entry whose literal has
    since been wrapped, reworded or deleted is red too, which is what keeps the
    register from decaying into a mute button.

    Four things a later reader would otherwise get wrong.

    It lexes rather than `gmatch`es. This repository's comments are prose and quote
    strings freely, so `body:gmatch('"(.-)"')` reports a paragraph ABOUT a string as
    a string, and a gate that invents an offender is a gate people learn to read
    past. It also handles LONG BRACKETS rather than walking past them, which the two
    siblings that landed this shape could skip and this one cannot:
    `settings/Spells.lua:50-51` holds two `[[Interface\...]]` texture paths, and a
    lexer that did not know what `[[` was would read the apostrophe in a later
    `spec's` as a string opener and silently lose the rest of the file.

    The two halves have deliberately different scopes. Key coverage reads every file
    the TOC loads outside `libs/`, because a key used anywhere and defined nowhere is
    the same defect wherever it sits. The residue half reads `settings/` and nothing
    else -- the surface the M4-21 acceptance criterion names, and the one
    `docs/common-tasks.md` makes its rule about. `/kcd` command output in
    `core/KickCD.lua` is about a hundred further prose literals, and the debug-console
    text in `modules/Castbar_Debug.lua` and `modules/Cooldowns.lua` is more; they are
    still bare English, are not scanned, and are now SAID to be so in
    `docs/common-tasks.md` and `docs/testing.md` rather than papered over. Routing
    one help listing across two locales is one decision about the command surface as
    a whole, and it is not this one.

    Forty-one residue entries, each carrying one of eleven classes argued once here
    rather than forty-one times: DIAGNOSTIC, VALIDATOR, DEGRADED STEM, DEGRADED
    FALLBACK, SPLIT COLOUR, FRAGMENT, LIB DESCRIPTOR, MEDIA KEY, FORMAT SUFFIX,
    CLIENT SUPPLIED, NOT YET ROUTED. MEDIA KEY is the one that would be a real
    mistake to route: "Friz Quadrata TT" and "Blizzard Raid Bar" are LibSharedMedia
    registry keys compared against what `LSM:Fetch` was registered under, and a
    translated one fetches nothing and renders the row blank. NOT YET ROUTED is
    deliberately unflattering -- five settings-page sentences would route cleanly and
    simply have not been, because M4-21 scoped this repository's routing to the three
    desc keys the finding names.

    The register also records something the scan found that I had not: `Death Knight`
    and `Demon Hunter` at `settings/Spells.lua:839-840` are consulted BEFORE
    `LOCALIZED_CLASS_NAMES_MALE`, so on a French client those two classes read
    English in the Spells dropdown while the other eleven read French. Filed as
    CLIENT SUPPLIED with the reasoning, not fixed: wrapping them would move the
    English into a locale file instead of ending the inconsistency, and the actual
    fix -- consult the client first, keep these as the no-global fallback -- is a
    behaviour change M4-21 does not scope and no client is available to check.

    Watched red in the working tree before any of it landed, in all three
    directions. With an empty register the acceptance case named all 41 sites by
    file, line and text, and the key-coverage case named exactly the three
    Castbar.lua descs. Adding a bare literal to `settings/General.lua` reddens the
    acceptance case naming it. Wrapping the recorded
    `settings/Panel_Widgets.lua:123` fallback reddens the staleness case naming the
    entry to drop. Restoring `L["Cast bar width in pixels."]` reddens the case that
    pins KICKCD-R-03 by name.

    847 -> 853 cases; `docs/test-cases.md` and the README badge move here.

    No smoke step. `NS.L` answers an unset key with the key, so all three descs
    assemble byte-identical output before and after this change and there is nothing
    in the client to look at; a smoke step that cannot fail is worse than none.

    Sibling shape followed rather than invented: PrettyChat 598dfbe and
    ConsumableMaster 2f5876b landed the same lexer, the same two directions and the
    same reasoned register. The classes differ where the tree does -- MEDIA KEY,
    FORMAT SUFFIX and CLIENT SUPPLIED are KickCD's, and DEGRADED FALLBACK is split
    out from DEGRADED STEM because `settings/Panel_Widgets.lua:123` is the `or` arm
    behind an L lookup rather than half a shared sentence -- and this one handles
    long brackets, which neither sibling needed. LootHistory declined the coverage
    clause at fca430e with a reason that does not apply here: KickCD routes 305
    strings through L already, so a bare literal beside them is a real and invisible
    gap.

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

commit 236f673e3c3840106619c37204add21cb221303a
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 15:28:54 2026 +0530

    M4-20: two dead arms go, and the register stops carrying a to-do

    The KickCD share of M4-20. Two deletions, both of them arms that can never be
    taken, plus the ARCHITECTURE.md paragraph that had been recording one of them as
    open. AbsorbTracker, ConsumableMaster and PrettyChat -- the other three
    repositories on the row -- are untouched here; each landed its own half.

    Both premises were re-opened in the tree before anything was cut, because the
    findings were triaged against a tree ninety-odd commits back and the plan's line
    numbers have all drifted.

    The bus fallback (KICKCD-R-09). tests/test_bus.lua:46 read
    `NS.NewBusTarget and NS.NewBusTarget()` and, on nil, built a bare table and
    embedded AceEvent into it, under a comment saying NewBusTarget "lands in Sprint
    3 (KCD-09)". It landed: core/KickCD.lua:46 declares it, settings/Spells.lua:1243
    uses it in production, tests/test_options_panel.lua:251 and
    tests/test_settings_log.lua:134 use it, and the very next case in this same file
    asserts it is a function. So the branch was a second, weaker bus path that ran
    in no configuration and quietly stood in for the real one if the factory ever
    vanished. It is now
    `assert(NS.NewBusTarget, "NS.NewBusTarget must exist (core/KickCD.lua)")()`,
    the shape docs/reviews/2026-09-07/02_PROPOSED_CHANGES.md's C-07 specifies.

    That assert was seen red before it was committed: renaming core/KickCD.lua:46's
    declaration in the working tree turned the case into
    `FAIL Addon SendMessage reaches a registered module target --
    ./tests/test_bus.lua:46: NS.NewBusTarget must exist (core/KickCD.lua)`, and the
    rename was reverted. The old branch swallowed exactly that mutation.

    The printer fallback (KICKCD-A-08). modules/Castbar_Debug.lua:134 bound
    `NS.Util and NS.Util.print or _G.print`; it is now `NS.Util.print`. Unreachable
    by construction, and the construction was re-read rather than taken on trust:
    core/CoreSetup.lua:2 sets `NS.Util = NS.Util or {}` unconditionally, the
    library-absent branch defines `Util.print` at :115 and then RETURNS at :126, and
    the library-present path defines it at :187. There is no load in which the
    guard or the fallback does anything.

    What a reader could get wrong, twice over. First, the plan and the audit both
    write the fix as `local print = NS.Util.print`; the local is called `emit` here,
    not `print`, because M4-17 already renamed all eight of this file's parameters
    off the shadowed global. The audit's wording predates that and is not a
    different instruction.

    Second, this is not purely subtractive. The guard and the fallback were one
    expression, so where a nil printer used to degrade into an untagged global
    print, it now raises. That is the trade the standard's rule wants -- an untagged
    dump pasted into a bug report is worse than a visible error -- but it is a
    behaviour change on a path nothing headless walks. tests/test_coresetup.lua's
    "the degraded printer is still secret-safe and still says <secret>" proves
    Util.print exists with no library; tests/test_castbar_debug.lua's 18 cases drive
    the dump WITH one. Nothing joins them: that helper loads T.load(true, ...)
    throughout. docs/smoke-tests.md gains section 31, which runs `/kcd debug castbar`
    inside section 25's already-renamed libs/LibKa0s_off, and it is folded into
    session 3 because that is this repository's outstanding seam run and it reuses
    25's setup rather than arranging the rename twice. NOT RUN: no WoW client was
    available.

    Scope held deliberately. Three other `_G.print` fallback arms survive --
    core/KickCD.lua:108, core/Compat.lua:452, modules/Cooldowns.lua:538. The audit's
    own step 2.1 gives its verification as "grep -rn '_G.print' modules/ settings/
    core/ is empty", which those three make unreachable, but neither KICKCD-A-08 nor
    the M4-20 row names anything but Castbar_Debug.lua, and the plan's standing
    non-goal is any change not traced to a finding. The over-broad grep is the
    audit's error, not three more deletions owed here.

    The ARCHITECTURE.md paragraph at :278-284 said the arm was unreachable, graded
    it Info, and said it should go the next time the file was touched. This is that
    time, so the note goes with it -- the deviation register is a record of live
    decisions, not a graveyard. Every other mention of either symbol is inside a
    frozen audit or review bundle and stays there.

    Counts. 847 -> 847. Neither deletion removes a case: KICKCD-R-09 replaces a
    branch inside a case body and KICKCD-A-08 touches no test, so docs/test-cases.md
    and the README badge correctly do not move, which is what the row's acceptance
    asks for once the deletions are counted. luacheck 0 warnings / 0 errors in 91
    files; 847 passed, 0 failed, 0 skipped; `lua tests/run.lua --list` diffs empty
    against docs/test-cases.md.

    One pre-existing count claim was left alone: smoke-tests.md's "the 26 surfaces
    above" in the subset index already said 26 against 30 sections before this
    commit, and correcting it is not traced to a finding.

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

commit a91a25c27d926adb3fe5f54dc105c8ec1f78381f
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 15:21:19 2026 +0530

    M4-19: the shared instance is restored on the red path, not only the green one

    KICKCD-R-04, this repo's share of C21. Three cases parked `units.focus.link` and
    put it back on the LAST STATEMENT of their body, and tests/_kit/framework.lua's
    runCase does `pcall(t.fn)` — so a case that goes red never reaches its own last
    line, and the cleanup only ever ran on the path where cleanup matters least.
    The premise still holds ninety commits on: the three sites the finding names are
    tests/test_schema.lua:575, :605, :631 and tests/test_options_panel.lua:550 today.

    The fix is the first of the two shapes 03_SPEC.md offers — a guaranteed-run
    wrapper, not a fresh `T.load(true)` per case. Two reasons for that choice. This
    repo already has the idiom, in tests/test_cooldowns_gates.lua's `withSecrets`:
    park, `pcall` the body, restore, re-raise at level 0 so the failure still points
    at the assertion that raised it rather than at the wrapper. And these cases lean
    on the module-level `renderedUnitPage` fixtures over the shared instance; a fresh
    full load per case buys isolation nothing here is asking for.

    `withFocusLink` and `withViewedUnit` live in tests/run.lua rather than in either
    suite because both suites park both pieces of state, and one guarantee is easier
    to keep honest than two copies of it. Two more sites went through them than the
    finding names — the shared Unit-picker restores at test_schema.lua:528 and
    test_options_panel.lua:549 — because they are the same defect in the same bodies
    and leaving them behind would have made the file read as if the distinction meant
    something.

    WHAT A READER WOULD OTHERWISE GET WRONG, and it is the part worth writing down:
    this is a guarantee, not the repair of an observed break, and I could not make it
    into one. defaults/Profile.lua:322 ships `units.focus.link = true`, so two of the
    three cases park the default AND force the default — their skipped restore put
    back precisely what was already there. Only the unlinked case leaves a value that
    differs, `false`. I leaked that on purpose from two positions, the top of
    test_schema and immediately after the linked cases, and both runs came back
    847/847 green. The viewed unit is looser still: renderedUnitPage writes it in
    nearly every unit-page case and restores it in none, so a leak there is
    indistinguishable from the suite running normally. So the cluster's "misleading
    cascade failures" is not a symptom this addon has today. It becomes one the first
    time a case renders a Focus page without seeding the flag, and that case inherits
    whatever the last failure left. Making the restore MEAN something — parking this
    state around every suite that writes it and walks away — is a larger job than
    M4-19 and is not in it.

    Acceptance, all watched in the working tree and reverted:

      ctx.unit = ctx.unit or Helpers.ViewedUnit()   -> FAIL the Unit banner retargets
                                                       the page and every tab follows it
      onSelect writes ctx.unit, not SetViewedUnit   -> FAIL the Unit picker is one
                                                       selection shared by every
                                                       per-unit page (+1)
      RenderLinkedUnit returns before the strip     -> FAIL a linked Focus draws the
                                                       strip FIRST … (+2)
      deadenStrip(buttons) removed                  -> FAIL a linked Focus's tab strip
                                                       is disabled and desaturated
      deadenStrip applied to the unlinked page      -> FAIL an unlinked Focus's tab
                                                       strip is left operable and
                                                       undimmed (+5)
      w:SetHighlight(1, 1, 1, 0.12) re-added        -> FAIL the linked-Focus note has
                                                       no hover highlight but is still
                                                       clickable

    Each then green on revert. Note the first line: the picker case does NOT redden
    under the first of the two mutations its own comment names, only under the second
    — a pre-existing imprecision in that comment, left alone here because rewriting a
    "red under:" note is not what this item is for. It is recorded rather than fixed.

    The runner's own inventory pin, also watched red both ways: dropping "test_schema"
    from SUITES aborts with rc=1 before any case loads ("exists but is not declared in
    the suites list … it is running zero cases today"), and renaming tests/test_units.lua
    aborts with rc=1 ("declared in the suites list (position 9) but is not on disk").

    luacheck 0/0 over 91 files; 847 passed, 0 failed, 0 skipped — identical either
    side, so docs/test-cases.md and the README [tests] badge do not move, and `--list`
    diffs clean against docs/test-cases.md. docs/testing.md gains the section naming
    the wrappers and the reason to use them, since a convention nobody can find is one
    the next case will not follow. No smoke step: nothing outside tests/ and docs/
    changed, so there is no in-game behaviour to observe. No version bump (Decision 5).

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

commit fe0539fa54a52eb4d1e7908a9d095ee791c8f724
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 15:05:15 2026 +0530

    M4-18: the reset that had two answers for one coordinate now has one

    This is KickCD's share of M4-18, and the share is exactly one finding —
    KICKCD-R-08. The cluster's other seven live in AbsorbTracker, ConsumableMaster,
    PanelMaster and PrettyChat, and nothing in this repository reaches them; three of
    those shares have already landed in their own repositories. The three sibling
    commits were read before anything moved here, and the shape they settled on is
    the shape this one follows: find the second copy of a value, delete it rather
    than correct it, and leave behind a case that would have caught it.

    The evidence was re-opened first and it still holds verbatim.
    settings/Panel_Render.lua carried a header saying the default coordinate comes
    from DEFAULT_PROFILE.units.target.anchors.icons "so we don't duplicate magic
    numbers across UI / CLI / Database layers", and then the statement directly under
    it duplicated the number anyway:

        or  { point = "CENTER", relativePoint = "CENTER", x = 0, y = -180 }

    against defaults/Profile.lua:314's `y = 120`. 300 px apart and opposite in sign,
    so the two answers are not a rounding drift — one puts the grid above screen
    centre and the other below it. The comment asserted an invariant the next line
    broke, which is the whole defect: the number was never going to be read, but the
    promise was.

    WHAT DOES NOT CHANGE, because it is the thing a reader is most likely to get
    wrong here. This is not a bug fix. The `or` arm needs NS.DEFAULT_PROFILE to be
    absent, and defaults/Profile.lua is a TOC-loaded file, so in any shipping install
    the `and` arm always wins and -180 has never reached a profile. Nobody's grid
    moved. That is also why the wrong number survived: an unreachable branch is
    invisible to the client and, until this commit, to the suite as well.

    The fix is the early return the finding asks for rather than a corrected
    constant, and the reason to prefer it over `or { ... y = 120 }` is that a second
    correct copy is still a second copy — it would go stale the first time anyone
    edits defaults/Profile.lua, silently, in a branch no test and no client visits.
    Doing nothing is also the right behaviour on its own terms: asked to restore a
    default that does not exist, the least surprising outcome is to leave the grid
    where the user dragged it. Note that the early return skips FireConfigChanged
    too, which is correct — nothing was written, so there is nothing to republish.
    That makes this function agree with its own sibling four lines below,
    Helpers.ResetAllPositions, which has guarded NS.DEFAULT_PROFILE and returned
    early since it was written.

    One case, and it was watched fail in the working tree before the fix went in:
    `ResetIconPosition writes nothing when the defaults tree is absent` in
    tests/test_settings_log.lua, which drags the anchor to TOPLEFT/400/-300, takes
    NS.DEFAULT_PROFILE away on the isolated instance, and asserts the stored anchor
    is untouched and that no CONFIG_CHANGED is published. Red against the old code
    with `expected TOPLEFT, got CENTER` — the fallback firing. Hand-removing the
    defaults table is the only way to reach the branch at all, and the case comment
    says so, so the next reader does not mistake it for a reachable scenario. The
    pre-existing case four lines above it still pins the arm a client actually
    takes, comparing against DEFAULT_PROFILE itself rather than against a literal.

    NO NEW SMOKE STEP, deliberately, and the reasoning matters more than the
    decision. docs/reviews/2026-09-07/03_SMOKE_TESTS.md §C-06 proposed one, but every
    path a client can reach through this function is byte-identical before and after
    — the surviving arm is the same table from the same source — and it is already
    covered headless. A smoke step here would be a human confirming behaviour that
    did not change. What §12's `/kcd resetposition` row was missing is different and
    worth fixing: it said the grid "snaps to its default screen position" without
    naming the position, and a check that only asks whether the grid MOVED cannot
    tell +120 from -180. It now names CENTER / CENTER, x = 0, y = +120 and says why
    the number is spelled out. No client was available and nothing here has been
    smoked.

    docs/settings-panel.md's paragraph on the helper claimed the default coordinate
    lives in one place, which was the same untrue sentence as the code comment; it
    now records the early return and what the deleted fallback said.

    Verified: luacheck . 0 warnings / 0 errors in 91 files; lua tests/run.lua 847
    passed, 0 failed. The count moved 846 to 847, so docs/test-cases.md is
    regenerated and the README [tests] badge moves in this same commit, and
    `lua tests/run.lua --list` diffs clean against docs/test-cases.md. Per Decision 5
    no version moves.

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

commit ae12650b6240fd8a92bf1331ba21f2e50ae01ebb
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 14:58:20 2026 +0530

    M4-17: the _G. prefix becomes a rule the suite can read

    This is KickCD's share of M4-17 — KICKCD-R-11 (bare WoW globals sitting beside
    _G.-prefixed reads) and KICKCD-R-12 (eight signatures in
    modules/Castbar_Debug.lua taking a parameter named `print`). The AbsorbTracker,
    ConsumableMaster and LootHistory shares landed in their own repositories and
    nothing here reaches them; WhatGroup's went in M4-11.

    The evidence was re-opened before anything moved, and the first thing it turned
    up is that the finding cites the wrong rule. KICKCD-R-11 grades itself under
    `architecture-§1`, but that section is the namespace bootstrap — `local
    addonName, NS = ...`, and MUST NOT create `_G[addonName]`. It says nothing about
    how a WoW API read is spelled. The rule that actually governs is this repo's
    own, docs/common-tasks.md § "Global lookup form", and reading it changes the
    answer in both directions.

    **Two of the three sites the finding names are not defects.** The doc has a
    "never guarded → bare X" bucket and `UnitClass` is in it, so
    `modules/IconGrid.lua:232` and `modules/Cooldowns.lua:80` are written the way
    the repo says to write them. Neither module guards the symbol. What made
    IconGrid.lua:232 look wrong was `_G.UnitExists` four screens above it at :148 —
    a DIFFERENT symbol, which the same doc puts in the prefixed bucket precisely
    because that file guards it. Both lines are unchanged, and core/Database.lua's
    `UnitRace`/`UnitClass` pair, which the same reasoning would have swept, is
    unchanged too.

    **The third site is real, and eleven more are.** `settings/Panel_Widgets.lua:121`
    guards `InCombatLockdown` and reads it bare, while core/KickCD.lua writes
    `_G.InCombatLockdown`. Beside it: `C_Timer` bare at core/Util.lua:123,
    settings/OptionsSetup.lua:126 and settings/Panel.lua:126; `IsLoggedIn` guarded
    and bare at modules/Cooldowns.lua:495; `UnitExists` at modules/Castbar.lua:896
    and modules/Castbar_Debug.lua:64 and `UnitName` at :69, all three prefixed
    elsewhere in the addon; `UnitIsDead` guarded as `X and X(...)` at Castbar.lua:897;
    `STANDARD_TEXT_FONT` at modules/UnitLabel.lua:89, whose twin at
    core/Constants.lua:110 is prefixed; the `or print` fallbacks at
    core/KickCD.lua:108 and modules/Cooldowns.lua:538, where core/Compat.lua:452
    writes `or _G.print` for the same expression; and `UnitClass` in the two modules
    that DO guard it, core/KickCD.lua:417-418 and settings/Spells.lua:137-138,
    :400-401, :1020.

    R-12 is the same defect wearing a different hat, which is why the two are one
    commit. `local print = NS.Util and NS.Util.print or _G.print` shadowed the Lua
    global through eight helpers, so thirty-odd `print(...)` calls in that file were
    reads of a local that no scan — and no reviewer skimming for
    `slash-commands-§4` violations — could tell from bare global reads. The
    parameter and the local are now `emit`, matching PrettyChat's
    modules/Override.lua, which threads its report sink under exactly that name.
    `settings/Slash.lua:61`'s `out(line)` is the same idea one level up; `out` was
    not reused because this repo already spends it on accumulator tables at five
    sites.

    **Why a gate, and why a narrow one.** tests/run.lua:83-97 publishes each
    instance's mock table AS `mocks._G`, so `_G.UnitExists` and `UnitExists` resolve
    through the same table — the two spellings are behaviourally identical in the
    harness and in the client alike, and no input a case can pass distinguishes
    them. The rule's entire value is that a reader can see which reads might find
    nothing there, so the only instrument that can measure it is one that reads
    source. That is how these sat under 845 green cases.

    tests/test_source_style.lua measures the doc's STANDING LIST and nothing else,
    and the narrowing is the considered part. The doc's other half — "guarded
    somewhere → `_G.X` everywhere in that module" — is not an invariant this tree
    satisfies: `LibStub`, `Settings`, `GameTooltip`, `Enum`, `DEFAULT_CHAT_FRAME`,
    `C_CooldownViewer` and `RAID_CLASS_COLORS` are all guarded and all bare, at
    roughly seventy sites. I wrote that gate first and it reported seventy-one
    offenders on a tree nobody considers broken. Its only green state is the blanket
    `_G.` sweep 03_SPEC.md § C31 names as a non-goal, and a gate that ships red
    teaches everyone to read past it. So the guard half stays advisory, the standing
    list is measured, and docs/common-tasks.md now says which is which instead of
    stating one rule and living by another.

    Invariant 1: the gate was watched failing in the working tree before the fix
    went back. With the case present and core/, modules/ and settings/ stashed, 845
    passed and 1 failed — forty-six offending lines across twelve files, the twelve
    sites above plus every shadowed `print` in Castbar_Debug.lua. Gate and fix are
    in this one commit; the branch is green at every commit.

    Documentation moved with the code, because three files described the old state.
    docs/common-tasks.md's standing list gains `IsLoggedIn`, `UnitIsDead` and
    `STANDARD_TEXT_FONT` (each guarded or used as a fallback, each already prefixed
    at its other site) and loses `UnitIsDead` from the bare bucket, names the gate,
    and records the guard half's residue rather than leaving a rule the tree does
    not follow. It also gains the `emit` convention, since a naming rule with one
    instance in the addon is a coincidence until it is written down.
    docs/ARCHITECTURE.md:278 cited `Castbar_Debug.lua:125` and `local print` — the
    line had already moved to :134 and the name is now `emit`; the note's Info
    grading and its open disposition are untouched, since removing the unreachable
    `_G.print` arm is a behaviour change and not this item's. docs/testing.md's
    source-scan section gains the new guard.

    What this does NOT do. It does not touch libs/ or tests/_kit/. It does not prune
    `.luacheckrc`: names now read only through `_G.` no longer need a `read_globals`
    entry, but pruning them would tighten lint for future bare reads, which is a
    separate decision with its own blast radius. And it does not correct
    ARCHITECTURE.md:269's `:35 / :82 / :85-86 / :102` citations into Castbar_Debug —
    they had already drifted before this commit and belong to the comment-citation
    cluster, not here.

    Verified: luacheck . 0 warnings / 0 errors in 91 files; lua tests/run.lua 846
    passed, 0 failed. The count moved by one, so docs/test-cases.md is regenerated
    and the README [tests] badge reads 846/846 in this same commit, and
    `lua tests/run.lua --list` diffs clean against docs/test-cases.md. Per Decision 5
    no version moves.

    No smoke step, and the claim is worth being explicit about because `/kcd debug
    castbar` is player-visible. There is nothing to look at. A `_G.X` read and a
    bare `X` read are the same table lookup in an addon file, so the twelve prefix
    changes cannot produce a different frame; and the `emit` rename is entirely
    local names, with all eighteen DebugDump cases in tests/test_castbar_debug.lua
    driving the renamed helpers through every arm — the deep colour and record
    branches included, because that suite sets `mocks.UnitExists` true rather than
    taking the early return. A smoke step here would be watching output that is
    provably byte-identical.

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

commit 894754524dd0d803a360402badc3c247769911f2
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 14:36:49 2026 +0530

    M4-16: the perf panel's close button goes back to the library

    The descriptor in core/PerfSetup.lua carried a `decorate` hook, and by the end that hook
    was a line-for-line copy of what libs/LibKa0s/PerfPanel.lua already does in its else arm:
    the same LibKa0s-Core factory, the same TOPRIGHT anchor, the same -(TITLE_H - 18) / 2
    offset. The field is gone and the library draws the control.

    What a reader would otherwise get wrong is why a duplicate this exact was worth deleting
    rather than leaving alone. The two arms are EXCLUSIVE — PerfPanel branches on `decorate`
    and runs one or the other — so this was never two paths agreeing today and kept in step by
    whoever edits both. For as long as the hook sat in the descriptor the library's own arm
    never ran once, in any client, ever, and nothing anywhere would have said so once the two
    drifted. The hook had earned its place: it began as
    `NS.DebugLog.MakeCloseButton(frame, api.Hide)`, a two-argument call onto a three-argument
    function, which is how this panel drew a multiplication sign with every suite green — a
    texture path that is never built draws nothing and raises nothing. Routing it through the
    addon's one wrapper fixed the drawing and made it IDENTICAL to the arm it was shadowing
    rather than different from it, and that is the point at which a private copy stops being a
    fix and becomes a liability.

    The deletion needed one source change to be safe rather than lucky. This descriptor passed
    the folder name only as `name`, and PerfPanel reads `d.addonName or d.name` — so the right
    answer was reached through a fallback. `title` here is already a third string ("Ka0s
    KickCD"), which is what a rename reaches for first, and the failure mode is the silent one
    this addon has been bitten by once already. So `addonName = addonName` is now stated
    explicitly, in the same shape and for the same stated reason as core/DebugLogSetup.lua's
    descriptor. Two descriptors in one addon disagreeing about how they answer the same
    question is its own trap. This is the one place KickCD's M4-16 differs from
    AbsorbTracker's, where the field was already there; it is the same change MultiMeters made
    for the same reason, and all three repos now end in the same shape.

    tests/test_perfsetup.lua gains a case rather than having one rewritten — unlike
    AbsorbTracker, nothing in this repo asserted the hook existed, so there was nothing to
    rewrite and the deletion would have been invisible to the suite. It pins both directions,
    `addonName` present and `decorate` absent, and then shows the REAL panel against a spy on
    LibKa0s-Core's factory, because a descriptor's shape says nothing about what reaches the
    screen. All three halves were watched red in the working tree before the fix landed: the
    name half against the descriptor as it stood, the `decorate` half against the undeleted
    hook, and the panel half against a PerfPanel.lua temporarily mutated to drop its third
    argument. The vendored file was restored byte for byte (md5 94445eb4…) and libs/ is
    untouched in this commit.

    The item's acceptance line asks that `grep -n 'decorate' core/PerfSetup.lua` return
    nothing. It does not, and deliberately: what is gone is the FIELD, and what is left is a
    reasoned block naming it, which is what AbsorbTracker and MultiMeters both landed too. A
    comment explaining why the hook must not come back is worth more than a grep that passes,
    particularly when re-adding one silently removes the library's control instead of adding a
    second.

    `NS.MakeCloseButton` (core/CoreSetup.lua) has no live call site left in this addon as a
    result, and the comment there said "today that is core/PerfSetup.lua's step panel", which
    is now false. It stays: it is the live half of a two-sided seam whose degradation branch
    publishes the same name, and tests/test_surface_parity.lua compares the whole namespace
    across a live and a library-less load, so deleting one side means deleting both. The
    absence is recorded at the definition rather than left to be rediscovered. If the
    collection's dead-export sweep reaches it before this addon draws a close control of its
    own, deleting the pair is the right answer.

    No WoW client was available. docs/smoke-tests.md gains §30 — one close control, in the
    corner, wearing the collection's mark rather than a multiplication sign, and hiding the
    panel without canceling the run. It is worth a login even though nothing on screen is
    supposed to change: the library's else arm has NEVER run in a client from this addon,
    because for as long as the hook existed it could not. §26's perf-panel bullet said this
    was "the one close button in this addon that the HOST builds" and no longer is, so it
    moves in this commit rather than in a later sweep. Nothing here has been smoked and
    nothing may be reported as passing.

    Case count 844 to 845: one case added. docs/test-cases.md and the README badge move with
    it.

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

commit 606d649295bc261c13fb26a6925a2dd386ccbe26
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 14:27:13 2026 +0530

    M4-12: the load-bearing TOC positions say what resolves at them

    `KICKCD-A-02` under `CX04`. The collection audit read `toc-file-§5:144`'s MUST
    against all 81 lines of this file and concluded the rule was unworkable; it was
    counting against the wrong denominator. `M1-STD-02` settled that upstream — the
    MUST binds positions that are load-bearing, and the denominator is established
    by reading the seam files and `core/Constants.lua`, not by counting TOC lines.
    Every premise below was re-derived in today's tree rather than taken from the
    triage, which was written against a tree ninety-odd commits back.

    The item names two positions for this repo. Re-derived against the standard's
    own denominator instruction, this file had FOUR, and the two extra are not
    padding — each fails silently, in the client, with no test going red, which is
    the exact failure class the rule exists for. All four are satisfied today by
    accident of the order someone typed.

      - `core\PerfSetup.lua` (the item's `KickCD.toc:55`). `modules/Cooldowns.lua`,
        `modules/IconGrid.lua`, `modules/IconGrid_Render.lua` and
        `modules/Castbar.lua` each hold `local Perf = NS.Perf` at file scope; the
        first bracket then reads `Perf.on`, so a `Perf` captured as nil raises on the
        first hot-path call rather than at load. The comment also records the second
        constraint — below `core\KickCD.lua`, because the descriptor's `version`
        falls back to `NS.VERSION` and every capture record stamped "v?" while this
        file sat higher (`performance-§8`).

      - `settings\OptionsSetup.lua` (the item's `KickCD.toc:73`; the file has since
        grown a line, so the position, not the number, is what was matched). It
        creates `NS.Settings` and publishes the `LibKa0s-Options-1.0` instance as
        `NS.Settings.Helpers`. Seven files take that member as a file-scope upvalue
        and `settings/General.lua:71` and `settings/Icons.lua:182` then CALL
        `H.AddComposed` at file scope, so a later position raises during load.

      - `core\CoreSetup.lua`, which the triage missed. It hangs the secret-safe
        printer on `NS.Util` as `NS.Util.print`; `core/Util.lua:12` assigns
        `NS.Util = Util`, a FRESH table, unconditionally. Reverse the two and the
        printer is replaced rather than merged, and because all ~20 call sites read
        it as `NS.Util and NS.Util.print`, the addon goes quiet instead of raising.
        `CoreSetup`'s own `NS.Util = NS.Util or {}` is what makes this survivable
        without an error, and therefore what makes it worth a TOC comment.

      - `settings\Panel.lua`, likewise missed. It decorates the Helpers instance in
        place with the members that did not generalise into the library —
        `Helpers.AddComposed` (`Panel.lua:191`), `Helpers.AnchorValues` (`:294`),
        `Helpers.LSMValues` (`:346`). `settings/Icons.lua:56` and
        `settings/Castbar.lua:192` resolve `H.AnchorValues()` into a file-scope local
        and three tab files call `AddComposed` at file scope, so `Panel.lua` is
        pinned above every per-tab file independently of `OptionsSetup`.

    What a reader would otherwise get wrong: `settings\Panel_Widgets.lua` and
    `settings\Panel_Render.lua` publish onto the same instance and yet carry
    nothing, which is correct, not an oversight. Nothing loaded later reaches any of
    their members at file scope, and their position is already pinned by the
    annotated line above them — `toc-file-§5`'s worked example is explicit that a
    rule making every line restate its neighbour's comment produces noise the next
    reader learns to skip. `core\EnvSetup.lua` and `core\PoolSetup.lua` keep their
    existing conventional notes for the same reason: `settings/Slash.lua:69` does
    read `NS.Version` at file scope, but across a group boundary that `§5`'s
    section-header MUST already pins.

    `core/CoreSetup.lua:60` cited TOC line numbers (`KickCD.toc:43, ahead of
    44/49/66/67`) that had already drifted by six and that these annotations move
    again. It now names the four files instead. That is the one non-TOC edit here
    and it is a direct consequence of this change, not a second item.

    No doc change is owed: `docs/ARCHITECTURE.md`'s load-order list already states
    all four constraints in prose (items 10, 15 and 19) and no document in this repo
    claims a COUNT of load-bearing positions, so there is nothing to move with the
    manifest.

    Comment-only; no file entry moved and no order changed. `#` lines are skipped by
    every TOC parser that reads this file, including `tests/_kit/loader.lua:122`
    (`not entry:match("^#")`), which is how all 844 cases load the addon — the suite
    passing is direct evidence the new lines are inert. The three tests that read the
    raw TOC anchor their patterns at column one (`^##`, `^core\`) and are unaffected.
    No smoke step is owed and none is claimed: the client-side surface is that the
    TOC still parses and still enumerates every addon file, which `docs/smoke-tests.md`
    §1 Fresh install + first login already covers.

    Verified: `luacheck .` 0 warnings / 0 errors in 90 files; `lua tests/run.lua`
    844 passed, 0 failed; `--list` diff against `docs/test-cases.md` empty. The case
    count does not move, so `docs/test-cases.md` and the README `[tests]` badge do
    not move. Per Decision 5 this cycle does not ship: no version bump anywhere.

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

commit 58971a18b39b2d2b0be719d27f77f2d8fefb747b
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 14:17:05 2026 +0530

    M4-11: the test tree comes into lint scope

    `.luacheckrc:4` excluded bare `tests/`, which is what `lint`'s own template shipped until
    `M1-STD-01` amended it, so all 55 of this repo's own test files sat outside the gate. `luacheck .`
    covered 35 files and reported 0/0; it now covers 90 and reports 0/0, and only the second figure
    means what every RESULTS.md table has been quoting it to mean. `tests/_kit/` stays excluded for a
    reason that does not generalise: it is a byte copy of LibKa0s' `testkit/`, linted there as source,
    so linting the copy would report every finding twice and let the copy drift green while the
    original went red — the one state `tests/test_vendor_sync.lua` exists to forbid.

    The harness global is declared in a `files["tests/"]` stanza rather than in the top-level
    `read_globals`, and the difference is not cosmetic. A name granted at the top level is granted to
    `core/`, `modules/` and `settings/` as much as to a suite, and a shipped file reaching for the test
    harness is exactly what this gate exists to refuse. It is spelled `_G.KICKCD_TEST` and not bare
    because `tests/run.lua:217` writes it through `_G` and every suite reads it back the same way —
    `globals`, therefore, not `read_globals`.

    **This repo's stanza names one global where three siblings name three, and that is deliberate.**
    AbsorbTracker, PrettyChat and WhatGroup each list their SavedVariables tables beside the harness
    global because a suite there writes `_G.<Addon>DB = nil` to assert on the absent-saved-variable
    path. No suite here does: the fixtures hand each instance its own `inst.mocks.KickCDDB`
    (`tests/test_color_shape.lua:154`), and the only mention of `KickCDPerfDB` in the tree is
    `tests/test_perfsetup.lua:60`, which asserts on it as a *string* inside the TOC. Copying the
    siblings' list verbatim would have granted a permission nothing uses, which is how a stanza stops
    describing the tree it guards.

    Narrowing the exclusion turned the tree red with 81 warnings. Forty of those were one thing: every
    `KICKCD_TEST.<field> = ...` in a suite read as `(W122) indirectly setting read-only field ... of
    global _G`, because the harness global was undeclared. The stanza is what answers those — a
    declaration, not a suppression. The remaining 41 are real findings in this addon's own code and all
    41 are fixed rather than ignored. An ignore wide enough to silence the wall would read as coverage
    in every table quoting the 0/0 and provide none, which is worse than the exclusion it replaces.

    Nineteen were positional discards spelled with an underscore *prefix* — `_NS`, `_p`, `_relativeTo`,
    `_point`, `_s` — which luacheck does not treat specially; only bare `_` is exempt. The linted half
    of this repo has always used bare `_` for a discarded return (`core/Database.lua:219`,
    `modules/IconGrid.lua:1001`), and reserves the `_name` prefix for file-private locals that ARE read
    (`modules/IconGrid_Render.lua:36`). The test tree had been quietly using the second convention to
    mean the first for as long as nothing checked it. They are now bare `_`.

    Twelve were redundant re-bindings that shadow a file-level name. Ten of those — seven in
    `test_schema.lua`, two in `test_options_panel.lua` — were byte-identical: `local NS = T.NS` inside a
    body whose file already binds exactly that at line 3. Deleting them changes nothing at runtime and
    removes the question of whether the inner one is different. The other two ARE different, and that
    is why they mattered: `test_options_panel.lua:571` and `:619` bound `NS` and `H` off a
    separately-loaded `inst`, so `H` inside those two bodies was a different table from the `H` every
    other case in the file uses, under the same name. They are `iNS` and `iH` now.

    Nine were dead bindings, each a reader's trap rather than a style point. `test_units.lua:5` and
    `test_castbar_helpers.lua:14` destructured an assertion nothing below them calls; in a file whose
    whole vocabulary is assertions, an unused one reads as a case someone forgot to finish.
    `test_perfsetup.lua:98` took the Castbar module and never drove it, in a case that exists to prove
    which brackets were reached — so the binding claimed a coverage the case did not have.
    `test_options_panel.lua:790` read `libs/LibKa0s/Options.lua` into a local and dropped it; the loop
    four lines above already reads that same file along with its two siblings, so the second read is
    what was left behind when the single-file check was generalised.

    `test_coresetup.lua:367` is the one worth arguing about. It saved `frame.AddMessage` and never
    restored it, sixty lines below a case at `:309-312` that saves and restores the same field
    correctly. The restore was NOT added, because the two cases are not the same case: `:308` reaches
    for the file-level `mocks`, shared by every case in the file, where the restore is load-bearing;
    `:366` reaches into a `T.load(...)` instance created three lines earlier and discarded at `end)`,
    where it restores a table nothing will read again. Adding a no-op restore to satisfy the warning
    would have made the save/restore pattern mean less everywhere it does matter. The dead save goes.

    One was a stub lying about its own signature: `test_cooldowns.lua:234` replaced `PollSpell(self,
    id)` with a function that answers the same table for every id. Naming a parameter it never reads
    told a reader the fixture varies by spell. It takes no arguments now, which is what it does.

    One was a vestigial accumulator: `test_schema.lua:277` built a `seen` set that `:287` wrote and
    nothing read — `closed` is what the group-contiguity assertion actually consults.

    `test_icongrid_render.lua:36` unpacked four channels from the array colour shape and asserted only
    the first and last, which is what left `g` and `b` unused. The honest fix was the assertions, not
    the discard: the hash-shape line directly below it already asserts all four, the array-shape case
    had simply been written to spot-check the ends, and `SafeUnpackColor` returning the middle two in
    the wrong order is precisely the defect a positional unpack test is for. The four literals are read
    straight off the fixture, so nothing is invented.

    The last is the only suppression in this change and it is one line wide.
    `test_perfsetup.lua`'s `firstWatchedSpell` returns on its first iteration, and `(W512) loop is
    executed at most once` is right about the control flow and wrong about the intent: that IS how you
    take an arbitrary element of a set in Lua, and the function's own comment has said so since it was
    written. It carries `-- luacheck: ignore 512` with that reasoning above it. The file's *second*
    copy of the same idiom, open-coded at `:576`, needed no directive at all — it was duplicating the
    helper defined 549 lines above it in the same file, and now calls it.

    `docs/common-tasks.md:213` told a reader that `tests/` is excluded outright, which was true when
    written and is now the opposite of the rule; it is corrected in the same commit and names the one
    directory inside the tree still carved out and why. A stale scope note is worse than none, because
    it is the document someone consults before trusting a clean run. `:216` said "there is no live
    example in this repo right now" of the inline-directive form. That sentence was true this morning
    and this commit falsifies it, so it now names `firstWatchedSpell` — the doc's own point, that the
    inline form is for a warning that is wrong about one file, finally has something to point at.

    `CLAUDE.md:90` still reads true: it claims 0 warnings and 0 errors and makes no claim about scope.
    No case count moves — 844 passed, 0 failed, before and after — so `docs/test-cases.md` and the
    README `[tests]` badge are untouched, and `--list` still matches the document byte for byte.

    Nothing here is reachable from the client. `.luacheckrc` is not shipped, the test tree is not loaded
    in game, and no file under `core/`, `modules/` or `settings/` is touched by this commit — so it adds
    no `Smoke, session N` step to `docs/smoke-tests.md`.

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

commit 94db22577eb2150370bbdd4d4bf4824fa32e7d56
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 14:06:35 2026 +0530

    M4-09: the parity cases name the surface instead of rebuilding it

    KickCD is one of the three repositories that already had a
    tests/test_surface_parity.lua, so this item is the rewrite half of the row rather
    than the addition half. The three LIBRARY-BACKED cases -- DebugLog, Slash's cli
    and Options -- move onto Kit.assertSurfaceParity's by-name form, which arrived
    with kit 15 in M4-01. Core does NOT move, and the reason is worth writing down:
    NS and NS.Util are this addon's own namespace, half-published by
    core/CoreSetup.lua's live arm and half by its stub. There is no major to look up,
    so the four-argument form is the right one. NS.Slash stays with it for the same
    reason -- it is the host's own table; only NS.Slash.cli is the library's.

    WHAT THE BY-NAME FORM CHANGES HERE IS MOSTLY NOT IN THE TEST FILE, and that is
    the thing a reader would get wrong. AbsorbTracker's copy of this item deleted
    twelve hand-written exemptions for `__`-prefixed library internals. This file
    carried only two of those (__pages and __print), because KickCD had answered the
    same pressure from the other side of the seam: settings/OptionsSetup.lua's stub
    MIRRORED eleven of the library's internals as no-ops, with a comment saying
    exactly why -- "the parity gate reads the WHOLE live surface, and a member that
    exists live and not here is a hole". Kit.publicMembers drops the whole prefix, so
    that reason is gone, and ten of the eleven were then no-op members with no caller
    anywhere in this addon: copies waiting to go stale on the next re-vendor that
    renames one. They go in the same commit that falsifies the sentence justifying
    them, because leaving them would leave a lie in shipped source.

    __panelFor is the eleventh and it STAYS, because this repo is the exception the
    kit cannot know about: settings/Panel_Widgets.lua:138's OpenPageTab reads
    `Helpers.__panelFor and Helpers.__panelFor(pageKey)` to pre-select the
    destination page's tab. A member the host calls is a member the stub owes -- the
    same sentence SetRenderer joined the no-op list under at CX03. The call is
    GUARDED, so losing the member would cost a tab selection rather than raise, which
    is the quiet kind of degradation nothing else in the suite would report; so the
    case pins it BY HAND on the line after the parity call rather than trusting a
    filter that is right about every other `__` member in the library.

    The load-bearing half of the test change is one table in tests/run.lua.
    Kit.expose auto-wires the mock's LibStub as the surface source, which is correct
    for a repo whose stubs mirror LIBRARY TABLES; all three of ours mirror an
    INSTANCE -- what lib:New(descriptor) returned. Watched fail in the working tree
    with the registration removed: Options went red on LAYOUT, New and STRINGS,
    DebugLog on MAX_BUFFER, New and STRINGS, and Slash on ten more. It sits BEFORE
    Kit.expose deliberately -- expose registers a source only when none is registered
    yet, precisely so a runner like this one keeps its own.

    One consequence of that, stated rather than buried: the live half is now the
    runner's shared instance instead of this file's own pristine `live` load. Same
    construction either way (loadInstance(true)), but earlier suites have driven it,
    and tests/test_debuglogsetup.lua:70 shows the debug console -- which stamps
    _frameForTest and _toggleClickForTest onto the instance. Single underscore, so
    Kit.publicMembers does not filter them and they are exempted by name with that
    reason. This was watched, not predicted: it was the only red the conversion
    produced.

    Every gate watched red in the working tree and reverted: __panelFor deleted from
    the stub gives "the stub owes __panelFor: settings/Panel_Widgets.lua:138 calls
    it"; RenderRows dropped from the no-op list gives "RenderRows is missing (live:
    function)"; Section set to `false` instead gives "Section is a function live but
    boolean degraded", the arm a present-but-not-callable member falls into and the
    one a plain "is the key set?" check waves through; ConsoleCheckbox renamed in
    core/DebugLogSetup.lua and CliList dropped from settings/Slash.lua's stub New
    each name themselves.

    NO SMOKE STEP, and the reason is not that it is inconvenient. The only shipped
    source that moved is the arm that runs when LibKa0s is ABSENT, and LibKa0s is
    vendored inside this addon's own folder -- there is no in-game state that reaches
    it. The ten deleted members have no caller in core/, modules/ or settings/;
    `grep -rn '__panels|__bannerBand|__layoutTabs|__releaseChrome|__scrollTopInset|
    __tabBand|__tabPlacement|__releaseSubTabs|__tabArtHeight|__resetTabArtHeight'`
    outside libs/ returns the stub itself and nothing else. Inventing a smoke step
    for a path a player cannot reach would be a step nobody can fail.

    844 passed, 0 failed, unchanged either side, so docs/test-cases.md and the README
    [tests] badge do not move and the --list diff is empty. luacheck 0/0.
    docs/testing.md's parity paragraph is rewritten in the same commit: it claimed
    the gate "walks the LIVE table", which is now true of Core alone.

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

commit 6eddbd430e9d20fd04e3fdb5222e4db1052f7e8b
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 13:07:14 2026 +0530

    M4-13: the British spellings go, against the published list

    localization-§5 now publishes a canonical BRITISH/ALLOWED pair (M1-STD-12) and
    requires every mechanical gate to read it whole. This sweep is that list run over
    KickCD's authored text: source comments, one locale-file comment, test case names
    and identifiers, and every live doc.

    Measured: 62 lines across 20 files. The audit said 51 hits in 12 files and the
    triage sweep corrected that to 37 in 11, because the audit's count had folded in
    frozen bundles. Both were greping colour, behaviour and grey. Against the full
    published list the wider entries turn up as well — recognise, generalise,
    standardise, centrepiece, judgement, acknowledgement, flavour, travelled,
    totalled, labelled, cancelled — and those are what makes the figure larger than
    either earlier count rather than smaller.

    Two things worth knowing. `tests/test_settings_spells_editor.lua:573` is not
    prose: `local cancelled` is an identifier, and §5's scope covers identifiers for
    the reason the section gives — a British-spelled name sits one letter from the
    Blizzard symbol beside it. And `docs/settings-panel.md:103`'s heading loses its
    `-colour-` anchor. The only inbound links to it are in `docs/audits/2026-09-07/`,
    which is a frozen bundle and stays as written; no live document links to it.

    One line keeps its spelling deliberately. `docs/smoke-tests.md:774` is the
    session-3 check that the v1.27.0 perf strings read US, and it quotes CANCELLED
    and unlabelled in order to say a double-L there means the string did not come
    from the vendored payload. Correcting the quote would delete the check. The
    prose around it at :781 and :785 is swept.

    One case name moved — the Spells editor's neighbour splice — so
    docs/test-cases.md is regenerated here. The count does not move, so the README
    [tests] badge stays at 844.

    The plan's verification grep still returns three lines in this repo, all inside
    docs/perf-analysis/ and docs/revendor/ — frozen dated bundles, which 03_SPEC.md
    names as a non-goal. Add --exclude-dir=perf-analysis --exclude-dir=revendor and
    it returns nothing.

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

commit 121da01170c22e581f2006a2b06bbb4cb150492f
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 13:04:42 2026 +0530

    M4-25: the tab-strip case was two cases wearing one name

    KickCD's last complexity warning was a test. lizard measured the anonymous
    function at tests/test_schema.lua:595-631 at CCN 18 against a threshold of 15 --
    29 NLOC, one warning in a repo of 2232 functions -- and automated-tests.md:143
    gates a tag on suites.complexity.warnings == 0. So the one thing standing
    between this addon and a passable release gate was a test case that had grown a
    second half.

    The seam was already written down. The comment at :613 opened with "And an
    UNLINKED page's strip is untouched", and the case's own `red under` line named
    two mutations rather than one: "dropping the disable pass, or applying it to an
    unlinked page". A case with two mutation triggers is two cases. It set
    cfg.link = true, asserted every tab disabled and desaturated, then set
    cfg.link = false, re-rendered, and counted operable and dimmed buttons on the
    result -- two setups, two renders, two invariants, one name and one pass/fail
    bit. Whichever half broke, the runner printed the same line.

    Split on that line, with no assertion added, removed or reworded. Each half now
    reads its own cfg, sets the link flag it needs and restores what it found, so
    neither depends on the other having run first. The prose split with the code:
    the INERT-strip paragraph stays with the linked case, the "counted rather than
    asserted per button" paragraph -- which explains why the unlinked half counts
    instead of asserting, the library disabling the selected tab on every strip --
    moves to the unlinked one, and each keeps the one mutation it is actually red
    under.

    Seen red, one at a time, in the working tree. Commenting out
    settings/Panel_Render.lua:218's deadenStrip(buttons) fails the linked case and
    only the linked case. Adding deadenStrip((ctx.__tabLayout or {}).buttons) after
    the RenderTabbedSchema call at :140 -- which is what "applying it to an unlinked
    page" means -- fails the unlinked case, along with five other per-unit page
    cases that also cannot survive a dead strip. Both mutations reverted; the
    working tree carries only the split.

    What a reader would otherwise get wrong: this changes no shipped code and fixes
    no defect. deadenStrip and its call site are untouched. The addon behaves
    exactly as it did; what moved is that the suite can now say which half of the
    contract broke, and lizard has nothing left to warn about.

    Counts move together, as they must: 843 to 844, docs/test-cases.md regenerated
    through `lua tests/run.lua --list` and the README [tests] badge in this same
    commit. No version bump anywhere -- this cycle does not ship, and the badge is a
    count, not a version.

    Verified in KickCD: luacheck . 0 warnings / 0 errors in 35 files;
    lua tests/run.lua 844 passed, 0 failed, 0 skipped;
    lizard -l lua -x './libs/*' -x './tests/_kit/*' . -- 0 warnings, against 1
    before this commit. No smoke step: no client-visible surface changed, and the
    M4-25 row carries none.

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

commit cbadcd84199eb1c6fb90907d80deb17a0e373f60
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 12:13:22 2026 +0530

    M4-04: the first of the five private Border patches goes

    KickCD is the first of the five addons to lose its core/LSMPatch.lua, and the
    first to be left with no fallback if the promoted surface is wrong. That is the
    point of doing this one repository at a time: five deletions, five commits, five
    places for a bisect to land if the client says the sentinel is not enough.

    What the file was. AceGUI's WidgetRegistry is process-global -- one slot named
    "LSM30_Border" for every addon in the client, Ka0s or not. This addon,
    AbsorbTracker, ConsumableMaster, MultiMeters and PanelMaster each shipped their
    own copy of the same wrapper, five distinct md5s, each registering at whatever
    version it found plus one. Load all five and the wrapper a Border dropdown
    actually got belonged to whichever addon the loader reached last. No suite in
    any of the five repos could see it, because each one loads a single copy,
    registers once and passes -- and docs/smoke-tests.md section 18 checked the
    alignment with KickCD alone, which is the same blind spot in the client.

    This commit carries the call site as well as the deletion. M4-02 put
    lib.__PatchLSM30Border() into AbsorbTracker's OptionsSetup and left the other
    four for their own items; KickCD's live wiring did not have it, so deleting the
    private copy on its own would have taken the fixup out of the addon entirely
    rather than moving it. The call and the deletion belong in the same commit for
    the harder reason too: a commit that removed the wrapper and left the gate for
    later is a commit that is red on the behaviour it claims to preserve.

    The gate. The new case in tests/test_options_panel.lua seeds a stand-in
    LSM30_Border into a fresh instance's registry through T.load's `mutate` hook --
    the only window before the addon's own sources run -- and then reads the slot
    back: it must no longer hold what was seeded, it must sit one version above it,
    and a second call must register nothing. Seen red on the call site alone, twice:
    once with core/LSMPatch.lua still on disk, which is the proof that the private
    copy was never visible to this suite (its PLAYER_LOGIN frame never fires
    headlessly), and once after the deletion.

    The comment on the call site deliberately does not spell the AceGUI entry point
    out. C02's acceptance is a grep for that identifier over core/, modules/ and
    settings/ returning nothing, and a prose mention is a hit an auditor has to read
    and dismiss. Same information, worded around the token.

    Docs follow the file rather than the feature: ARCHITECTURE.md and module-map.md
    lose the entry and renumber their load-order lists, settings-panel.md and
    smoke-tests.md section 18 now name the library member, and common-tasks.md says
    plainly that its inline-luacheck-directive example is gone and the repo has no
    live one left, rather than quietly citing a file that does not exist.

    Watch for, in the client: docs/smoke-tests.md section 29 is the session-5 step
    and it has NOT been run -- no client is available here. It is the only check
    that can see the defect at all, and this run is also the first evidence that one
    library-level registration dresses a dropdown in an addon that no longer carries
    its own. Four addons still hold their private copies while it runs.

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

commit a606707aed6fcf14365a8a845380bc17b74a63ab
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 11:28:07 2026 +0530

    M4-01: adopt LibKa0s v1.27.0, and wire the gate that came with it

    Both payloads move whole from the v1.27.0 tag -- `libs/LibKa0s/` and `tests/_kit/`
    -- and the CLAUDE.md provenance line moves with them, because that line is what
    `tests/test_vendor_sync.lua` reads to decide which tag to check the bytes
    against. A line ahead of the bytes and a copy taken from untagged master both
    fail there, which is the point of it.

    `diff -r` between a clean `v1.27.0` checkout and each vendored tree is empty
    BYTE for byte, not merely content-with-CR-ignored.

    TWO THINGS IN THE PAYLOAD REACH THIS REPO'S OWN FILES, and invariant 1 says a
    gate and its fix land together rather than leaving the tree red between commits.

      * The kit now SHIPS a suite. `tests/_kit/test_eol.lua` arrives with kit 15 --
        the working-tree line-ending gate, over every path `git ls-files` reports --
        and `Kit.assertSuiteInventory` fails the whole run until the runner declares
        it, deliberately, so a gate cannot arrive with a re-vendor and then quietly
        run nothing. It joins `SUITES` with its own `dir`, root-prefixed like every
        other path this runner builds, which also satisfies the second inventory
        assertion `tests/test_coresetup.lua` makes over the same list. That is the
        +1 in the case count, and `docs/test-cases.md` and the README badge move
        with it here. It passes on this tree: the only tracked file that is LF on
        disk is the kit's own `.sh`, which `.gitattributes` pins that way on purpose.
      * Options minor 8 publishes `O.__print`, the one instance print sink the shell
        hands OptionsWidgets so it stops building a second one from the same
        descriptor. This stub is load-completing rather than member-answering, and
        the host calls nothing on `__print` -- it is the library talking to itself
        across a file boundary -- so it is recorded as live-only DATA beside the
        layout constants, which is exactly the decision the header of that case says
        a re-vendor is supposed to force. The library's own comment claims a stub
        need not mirror it because `Kit.assertSurfaceParity` skips the `__` prefix;
        that is true of the kit's BY-NAME form and not of the four-argument form
        this case uses, which walks every key of the live table.

    No version bump anywhere: this cycle does not ship. The `[tests]` badge is a
    count, not a version.

    The client-only half is written into `docs/smoke-tests.md` as § 28, and the
    re-vendor subset at the foot of that file now names it. NOT claimed here.

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

commit ffc1ff8ab9160b5dc399b65281242f33cc4d6ec9
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 11:02:46 2026 +0530

    M4-C1: say when the vendored-copy diff is supposed to be non-empty

    docs/testing.md gave four `diff -r` commands against `../LibKa0s` and marked two
    of them "MUST be empty". Neither can be empty right now, and neither is wrong.
    The library is tagged v1.27.0 and this addon vendors v1.26.0, so the library diff
    reports 306 lines and the test-kit diff 947 — measured just now, not estimated —
    and it stays that way for the whole of M4 until this repo re-vendors. A gate that
    is red by design and does not say so trains a reader to ignore it, or worse,
    invites them to re-vendor an untested library release purely to make it quiet.

    What the section was missing is that the four commands compare against the
    sibling checkout's WORKING TREE — whatever `../LibKa0s` has checked out — which
    is a different question from "is the vendored payload the release this addon
    claims". Those two questions only agree while the library has tagged nothing
    newer than this addon has taken, and the gap between a release and the re-vendor
    carrying it is normal rather than exceptional.

    So the note names the authoritative check: against the TAG the root CLAUDE.md
    names, extracted from the provenance line and pulled out of the sibling with `git
    archive`. Run here it reports zero lines for both payloads, which is the answer
    that means something. And it is not new work — tests/test_vendor_sync.lua already
    asks precisely this inside the suite, greping the tag out of CLAUDE.md and
    reading that blob from git, so a green suite has answered it. The run below is
    0 SKIPPED, which matters more than usual here: those cases degrade to a skip when
    the sibling or git is missing, and a skip is not a pass.

    The two inline "MUST be empty" comments become "empty vs the CLAIMED tag" so the
    code block no longer contradicts the paragraph under it, and the working-tree
    diffs are re-framed as what they honestly answer — how far behind the library
    this addon is, which is release planning rather than a gate.

    docs/ARCHITECTURE.md carried the same MUST in one sentence of its LibKa0s bullet,
    so it is corrected in the same commit and points at docs/testing.md rather than
    restating the rule — one place to be wrong is enough.

    Verified here: luacheck 0 warnings / 0 errors in 36 files; 841 passed, 0 failed, 0 skipped, 841 total; the test inventory is in sync with
    docs/test-cases.md. Documentation only — no behavior, no test moves, no
    case-count change, so docs/test-cases.md and the README badge stay put. No client
    needed and no smoke step added: nothing here reaches the game.

    No version bump anywhere: this cycle does not ship.

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

commit e3274f6efd3a097359989da72b74ee863c910050
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 09:53:53 2026 +0530

    M3-01: re-vendor LibKa0s v1.26.0, and keep the media rows deferred

    The vendored payload moves from v1.25.0 to v1.26.0 — libs/LibKa0s/ and
    tests/_kit/ copied whole out of the tag, and CLAUDE.md's provenance line rolled
    in this same commit, because tests/_kit/vendor_sync.lua reads that line as an
    input and a line that disagrees with the bytes is the drift it exists to catch.
    Only two library files actually move: OptionsCompose.lua (COMPOSE_MINOR 2 → 3,
    the Critical) and OptionsWidgets.lua (minor 13 → 14, the pooled tab art). The
    kit is byte-identical between the two tags, so its copy is a no-op that was
    still made, because the payloads are vendored whole or not at all.

    The plan expected no KickCD code change here. That expectation was wrong, and
    the reason is worth writing down, because it is the sort of thing the next
    re-vendor will step on again.

    LIBKA0S-A-01 — the collection's only Critical — is that the composers wrapped
    O.LSMValues in a second closure while O.LSMValues already returns one, so
    enumList unwrapped once, saw a function, and handed the dropdown an empty list.
    v1.26.0 drops the outer wrapper: a composer now reads O.LSMValues ONCE, at
    row-declaration time, and assigns what comes back straight into values.

    KickCD never had the empty dropdowns, and that is not luck. settings/Panel.lua
    SHADOWS the library's O.LSMValues with a host implementation that returns the
    hash itself rather than a reader — a deliberate, documented deviation with its
    own register entry in docs/ARCHITECTURE.md. Against the old double wrap, that
    shadow cancelled the defect exactly: the composer's own closure deferred the
    read, so the dropdowns filled. Against v1.26.0 it becomes the defect the tag was
    cut to prevent, and the worse-shaped one — the composer reads the shadow while
    settings/Icons.lua is still being parsed, so every composed media row freezes at
    whatever LibSharedMedia happened to hold before any media addon has run. No
    error, no empty control, nothing to look at: a list that simply never grows.
    This is precisely what M3-02 says is non-optional for MultiMeters. The plan
    catalogued MultiMeters' table-returner and missed KickCD's, so the same edit is
    made here, in the same commit as the bytes that require it: Helpers.LSMValues
    returns a closure over the body it used to run inline. The listing, the ordering
    and the "Default" fallback are unchanged; only the shape moved.

    Three cases were red on the re-vendor alone and are the evidence for all of the
    above — test_color_shape's "an LSM-backed row resolves its values at call time"
    and "every static dropdown declares its order", and test_schema's default-in-
    values check, which between them found sixteen media rows that had turned into
    static hashes. No case was added or renamed, so docs/test-cases.md and the
    README badge do not move.

    docs/ARCHITECTURE.md's register entry and docs/settings-panel.md both stated the
    shadow returns a table. They now state the contract that replaced it, next to
    the case that holds it there.

    Smoke section 27 is new and is unrun: no client is available. It is the only
    place the deferral itself can be checked, because the failure needs a
    LibSharedMedia that fills after the schema files have been read.

    Verified: diff -r --strip-trailing-cr and plain diff -r are BOTH empty, for
    libs/LibKa0s/ and tests/_kit/, against a v1.26.0 export. luacheck 0/0 in 36
    files; lua tests/run.lua 841 passed, 0 failed; --list matches docs/test-cases.md.
    No version bumped anywhere — this cycle does not ship.

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

commit 01b535d6e1908d25965b6b611b5be58fa324d75b
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 01:38:07 2026 +0530

    M2-22: the secret arm of the castbar dump reports something, always

    reportSecretNint is the DEFAULT arm of NINT_REPORT: a boolean gets one line, a
    nil gets another, and everything else -- which on a 12.0 client means every
    protected interrupt in combat, the case the dump exists for -- lands here. Its
    entire body sat inside `if _G.C_CurveUtil and ...` with no else, so on a client
    without the evaluator the dump announced the field was secret and then said
    nothing more about it. Whoever is handed that paste cannot tell a dump that
    could not render the value from one that had nothing to render.

    Availability is a clause of the sentence, not permission to speak: the line goes
    out either way and names either the evaluator or its absence. The value itself
    is still only described -- never tostring'd, never formatted -- which is what
    the arm was always for.

    Two comment lines went with it. "Pass to FontString:SetText via a hidden frame
    to render and read back. Cleanest: just say secret and trust the curve." is one
    half of an argument with an approach nobody took, sitting above code that does
    neither thing, and a reader has to work out it is describing a road not taken.

    The case that pinned this was already here, asserting the dump printed no state
    line when the evaluator was missing -- a characterization case doing its job,
    which is to make the shipped shape visible rather than to bless it. It is
    inverted, not added to: the two claims cannot both hold. It failed first,
    reporting "  duration: nil" where the secret line belongs, then 841 passed, 0
    failed. luacheck 0/0 in 36 files. The four-suite run is green; its single
    complexity warning and max CCN 18 are what this repo already reported before
    this commit, and neither moved.

    Smoke 15 gained the with-and-without-C_CurveUtil dump, UNRUN -- no client is
    available here, and half of it wants a client that lacks the evaluator at all,
    which Retail is not. The case count does not move: one case was renamed, not
    added, so docs/test-cases.md changes by a line and the badge stays at 841. No
    version moves; nothing in this cycle ships.

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

commit db1d396ad3e2b99d8736df42b4da4221c93c2510
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 01:28:31 2026 +0530

    M2-19: the biggest download in the collection sheds 7.5M it never needed

    KICKCD-A-01 was filed as a dotfile gap -- .claude, .superpowers and .pkgmeta
    missing from the ignore block -- and on that framing it is a Low. Re-measured
    against git rather than against the working directory, the dotfile half is worth
    exactly zero bytes: .gitignore names .superpowers/ at :29 and .claude's only file
    at :28, `git ls-files` returns nothing for either, and the packager clones git.
    Those lines are bookkeeping and their comments now say so, verbatim, so the next
    reader does not mistake them for savings.

    The real number was sitting one directory over and no audit asked for it.
    media/screenshots is seven tracked files, 7,813,732 bytes, and 4.7M of that is a
    single compressed .gif. It is the largest such payload in the collection, it is
    project-page art the CurseForge CDN already serves, and every player has been
    downloading it. CLAUDE.md and DEPENDENCIES.md go with it as developer
    documentation; README.md and LICENSE stay, because players read those.

    The bookkeeping lines are still required, and the reason matters. packaging.md:28
    carries a strong form: every root dotfile and dot-directory present in the repo
    MUST either appear in this list or be justified beside it, tracked or not. The
    point is not the package size -- it is that an enumeration goes stale silently
    the moment a new tool writes a new directory, and by then nobody is looking.
    .pkgmeta names itself under the same rule; the standard's own minimum template
    left it out until v2.39.0 and cites this file among the seven that failed the
    check on a line the template never supplied.

    Verified with `git ls-files` filtered through this file's own ignore: list, which
    is the only check that exists here -- no repo carries .github/workflows and
    packaging.md:32 puts CI out of scope, so there was never a zip to list. Run
    before the edit it named nine shipped files at 7,834,268 bytes; it now reports
    237 tracked files shipping, and no screenshot or dev document among them.

    841 tests pass, luacheck 0/0. No version moves; nothing in this cycle ships.

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

commit 3b67e1d4b8bfbefe48c1c86ae5a091da0628ee67
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 00:24:14 2026 +0530

    M2-17: the Profiles and Spells pages onto SetRenderer

    The Blizzard AddOns sidebar reaches a canvas panel without going through
    OpenOptionsPanel, so it never meets the combat gate `/kcd config` has. That
    is why the refusal lives in the library's SetRenderer instead, at the OnShow
    it installs. Four of this addon's six pages have been on it since the tabbed
    rewrite; Profiles and Spells parked their own OnShow and therefore had no
    guard on the one path a player is most likely to take mid-fight, while
    docs/settings-panel.md described a refusal covering all of them. Both now
    hand their body to H.SetRenderer, and Spells stops calling
    EnsureDefaultsButton itself because the library already does it first.

    The thing a reader will get wrong is the timing. SetRenderer runs a renderer
    on first show and then only when the page has been marked dirty, and both of
    these pages need every show: Spells releases its entire widget tree back to
    AceGUI's pool on hide and would draw an empty page the second time, and
    Profiles re-Opens the AceConfigDialog precisely so it describes the profile
    that is active now. So both say so, from their own OnHide, through
    H.RefreshPanel(ctx, true) — the published way to mark a hidden page dirty,
    published because a host that guessed at the private flag instead is a bug
    this collection has already shipped once. Declaring a renderer does newly
    put Profiles on the RefreshAllPanels fan-out, but nothing fires that on a
    profile switch, so the OnHide is what keeps the behaviour rather than a
    belt-and-braces second path.

    SetRenderer also moves out of tests/test_surface_parity.lua's exemption list
    and into the degradation stub. It had been exempted on the argument that a
    page which cannot be built has nothing to render into — true, and equally
    true of CreatePanel and EnsureScroll sitting stubbed beside it. That
    exemption is how AbsorbTracker's stub came to omit the member outright with
    its own parity case green, so it is not granted any more. Removing the
    exemption without the stub member fails the case with "SetRenderer is
    missing (live: function)", which is the shape this was supposed to have.

    The sidebar walk is written into docs/smoke-tests.md § 14 rather than left
    in the review bundle. It is a different path from the `/kcd config` check
    directly above it and a green there proved nothing about these two pages,
    which is worth saying at the step rather than in a directory nobody opens.

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

commit 2373b215a54871676419533e2d1d737e7b671d85
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Mon Sep 7 19:21:35 2026 +0530

    docs: commit the frozen 2026-09-07 review and standards-audit bundles

    Produced by /wow-addon:review and /wow-addon:standards-audit as part of a
    collection-wide pass over all nine addons and LibKa0s. Both passes re-ran this
    repo's out-of-game suites from scratch first, so every finding rests on the
    numbers measured that day rather than on the committed record.

    Frozen dated artifacts. Never rewritten after the fact — a later correction
    belongs in the next dated bundle, not in this one.

    The consolidation these fed is Ka0sAddonsCommonTasks
    docs/2026-09-07-REVIEW_AND_STANDARDS_AUDIT_REMEDIATION/, which triaged both
    bundles against the code, re-graded severity by reachable impact, and dropped
    what did not survive re-reading. Read the consolidation for what is actually
    being done; read these for the evidence behind it.

    No code, TOC, doc or config file outside docs/ was touched by either pass.

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

commit 2e21463c352304a2e8f1f22e6bb55bd295745726
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Sep 3 18:24:17 2026 +0530

    docs(deps): point the `file -b` note at the line it actually lives on

    The CRLF-troubleshooting entry cited docs/testing.md:107; the `file -b`
    recipe sits on 108. A one-line miss, but this file's whole contract is
    that every entry names a file:line a reader can open — a citation that
    lands on the wrong line is the thing that costs the list its credit.

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

commit 2e21463c352304a2e8f1f22e6bb55bd295745726
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Sep 3 18:24:17 2026 +0530

    docs(deps): point the `file -b` note at the line it actually lives on

    The CRLF-troubleshooting entry cited docs/testing.md:107; the `file -b`
    recipe sits on 108. A one-line miss, but this file's whole contract is
    that every entry names a file:line a reader can open — a citation that
    lands on the wrong line is the thing that costs the list its credit.

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

commit 849f1710cbdfaa959b45c0df4f759f3354cc4c06
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Sep 3 18:03:20 2026 +0530

    Carry LibKa0s v1.25.0

    One file moved: OptionsCompose.lua, COMPOSE_MINOR 1 -> 2. MasterControls takes a
    `leadButton` -- one page-wide act of the host's own, drawn beside options-ui-§15's
    reset buttons: into the pair's empty right half on a frameless addon, or on its
    own row above the full pair on a framed one. It exists because §15 fixes the
    resets' wording and the composer is the only thing that writes it, so an addon
    wanting a button beside them had to keep a second copy of "Reset all settings" in
    its own source.

    NOT ADOPTED HERE, and the reason is structural rather than a preference: this
    addon passes the composer's afterGroup tail through unchanged, with no single
    page-wide act of its own for the seam to place. A leadButton with nothing to put
    in it is a field set to nil. Nothing is filed as a decline -- a decline records a
    decision about work that was offered, and nothing was.

    Both payloads copied WHOLE from the tag (git archive v1.25.0), never file by file
    and never from the sibling working tree: tests/test_vendor_sync.lua resolves the
    tag the provenance line names and compares both payloads against it, so a copy
    taken from a dirty checkout passes a local diff and then fails the gate. The
    provenance line moves in this same commit, which is what that gate reads.

    The kit is unchanged at revision 14 and is copied whole anyway: the two payloads
    move together so a consumer can never hold a kit that cannot compare the library
    it ships.

    No cross-major skew, and no file removed upstream -- so no deletion inside libs/
    was warranted.

    Frozen bundle at docs/revendor/2026-09-03/.

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

commit 0641f80f8d71e3639c71bbcd369cb37b30cdd35b
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Sep 3 13:23:08 2026 +0530

    feat(settings): one Unit picker, a link out of the linked-Focus note

    Four things about the Focus link, which is one relationship the panel was
    describing in four awkward ways.

    THE UNIT PICKER IS ONE SELECTION. It lived on each page's ctx, so choosing
    Focus on Icons and walking to Cast bar arrived back on Target: the reader
    re-picked the unit on every page and nothing on screen said why it had
    moved. Helpers.ViewedUnit / SetViewedUnit read one field on NS.State and
    RenderUnitPanel reads it rather than defaulting from the ctx. Session-only
    and deliberately NOT a SavedVariable -- where someone is looking is not
    something they configured, and persisting it opens a fresh login on
    whichever unit was picked weeks ago with nothing explaining it. Selecting
    now publishes a STRUCTURAL refresh, because the other two pages are showing
    the wrong unit the moment it changes.

    THE LINKED-FOCUS NOTE IS A LINK. It named a control and the page holding it
    and left the reader to find both by hand, two categories away in Blizzard's
    list. The phrase naming the destination now wears Blizzard's link blue and
    the line opens General already ON its Units tab -- activeTab set before the
    switch, so it does not jump a frame later. It refuses under combat for the
    reason /kcd config does: the category switch is protected, and calling it
    under lockdown taints the panel for the session (options-ui-§2). The page's
    category id comes from a new NS.Settings.categoryFor, filled by wrapping
    each builder, because the library's registry drops the builder's return and
    the id is otherwise unrecoverable.

    Two supporting members, both in the host's own widget file: Helpers.LinkRow
    (a full-width InteractiveLabel -- the whole line takes the click, since
    AceGUI has no widget mixing clickable and static runs in one string, and
    the colour on the middle phrase is what says where it goes) and
    Helpers.OpenPageTab.

    NO HOVER HIGHLIGHT ON IT. The first cut passed SetHighlight(1, 1, 1, 0.12);
    AceGUI forwards that to Texture:SetTexture, whose four-number form is the
    deprecated colour API, and the client painted a solid bright-green block
    across the whole line on mouseover. There is no correct number to pass --
    a highlight would have to be a texture path -- and the link colour already
    marks the line as clickable.

    A LINKED FOCUS'S STRIP IS INERT: every tab disabled, every one of their
    textures desaturated. Safe precisely because no schema row sets
    alwaysPerUnit any more, so every tab of a linked page draws the same note --
    a clickable strip there redraws the identical page, which is the same
    "looks like a control, isn't one" defect the ↑/↓ arrows had. The strip still
    DRAWS, because the page must not change shape when the picker flips
    (options-ui-§13). Done on the buttons TabStrip returns rather than a flag on
    the tab spec; if a second page ever needs it, that is when it moves into
    LibKa0s.

    THE LINK'S TWO CONTROLS SHARE A LINE, [tick][Copy styling from Target],
    through H.RenderGrid. They were two rows -- a half-width tick, then an
    InlineButtonPair holding one button -- and a button alone on a line reads as
    belonging to whatever follows it. They are two halves of one decision.
    InlineButtonPair could not do it: it owns a full-width row of its own.

    The harness could see none of this. Frame SetEnabled / SetDesaturated /
    GetRegions and the AceGUI widget's SetHighlight were PascalCase no-ops, so
    "did the page disable this?" and "no highlight" vs "a highlight nobody
    meant" had no answer -- which is exactly how the green block shipped.

    Verified: 841 passed / 0 failed (5 new cases, each confirmed red against the
    old behavior first), luacheck 0/0 over 36 files, tests/perf.lua clean,
    lizard 0 warnings at CCN 15 (OpenPageTab landed at 16 and was split).

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

commit 213c77743f4f2e9962232c7169958189b7ba58e6
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Sep 2 21:55:04 2026 +0530

    docs(comments): correct citations the revamp left pointing at nothing

    Comment text only. A comment naming a file, a line or a caller is documentation and
    drifts exactly like a README, except that nothing reads it but the next person to
    touch the function and no gate can see it — luacheck does not read prose and no test
    covers a comment.

    The recurring one is afterRestoreAll, a descriptor hook that stopped existing when
    the global reset became a profile reset, still being cited by name. Alongside it: a
    line number past end of file, a caller that does not call, and counts that no longer
    match what the code returns.

    Deliberate history was left alone — a comment explaining what a deleted file used to
    do is doing its job, and rewriting it would delete the explanation.

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

commit e2f386a1b5a8ec6f024b68f12af4c239531afb18
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Sep 2 18:25:03 2026 +0530

    docs: sync the doc set to the revamped settings panel

    The settings surface moved a long way in one pass — a Master controls tab, a strip
    on every page, class-colour companions, the library's composers in place of
    hand-written groups, and in some addons whole pages folded into General — so the
    docs describing it had drifted correspondingly far.

    Counts re-derived from the schema rather than carried across, the LibKa0s payload
    description corrected to the file it gained, and the documentation map reconciled
    against the directory in both directions.

    Documentation only: no .lua, .xml, .toc, .luacheckrc or .pkgmeta was touched, and
    the suites are unchanged and green.

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

commit c5682dda9cf7044cf0ca2147a796030d8b571b34
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Sep 2 17:56:13 2026 +0530

    feat(settings): annotations re-laid out, cast bar split, spells by drag

    Annotations gains a font shadow and the requested row order. Cast bar > General
    splits into Size and position and a new Icon tab, and Font gains a colour. The
    Spells page gains a strip and its list becomes the shared reorder widget.

    Label text rendered as an empty dropdown because it was declared type=string with
    no values and no dialogControl, so the library's dispatch made a dropdown with
    nothing to list. It was always meant to be free text: it now declares EditBox
    explicitly, and the re-vendored library warns on the bad shape so the next one
    announces itself.

    Re-vendors LibKa0s v1.24.0 and adopts the settings-revamp-v2 contract: a
    General page opening on a composed Master controls tab, a tab strip on every page,
    a class-colour companion beside every colour control, and the canonical font,
    border and bar groups built by the library's composers rather than spelled out
    per addon.

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

commit 2cf78fe7f089966fa46342f857380234da069a11
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 1 19:33:18 2026 +0530

    Tab the settings pages, and pin the Unit picker above the strip

    Every schema page was one long scroll. Icons ran 32 rows nose to tail, the cast
    bar 42, and finding "auto-size" meant scrolling past everything that was not
    auto-size. The library could not draw anything else until v1.23.0; it can now
    (options-ui-§13 / §14), so the four schema pages adopt it.

    THE STRIPS. Partitioned from the rows' own `group` field, in declaration order,
    with no second list naming the tabs — the schema array's order IS the strip's:

      General    2 tabs / 7 rows   Master controls | Units
      Icons      6 tabs / 32 rows  Sizing | Layout | Visual states | Border |
                                   Annotations | Ready glow
      Cast bar   7 tabs / 42 rows  General | Position | Font | Spell name |
                                   Cast time | Interruptible | Non-interruptible
      Text Label 3 tabs / 14 rows  General | Placement | Font

    Counts are per unit on the three unit-scoped pages. Spells and Profiles draw no
    strip: one is a bespoke editor with no schema rows, the other is AceDBOptions
    drawing a page this addon does not own.

    THE UNIT PICKER STAYS OUT OF THE STRIP. It is the page banner (Helpers.PageBanner,
    §14), pinned in the chrome band ABOVE the tabs. A unit is not a subject the page
    covers, it is the SCOPE every tab on the page is read under — and the mechanics
    agree with the design: a tab click clears the scroll, so a picker rendered into
    the scroll looks right on the render that drew it and vanishes on the reader's
    first tab click, which no static reading of the builder shows. tests/test_schema.lua
    pins it into the chrome for that reason.

    The hand-built unit header is gone with it — the Dropdown, the focus link tick and
    the copy button that Panel_Render drew three times over, once per unit page.
    There is exactly one link tick and one copy button now and they live on
    General → Units, because there is exactly one question (does Focus keep its own
    appearance) and the scroll is the only place left to draw them on a tabbed page.
    Helpers.RerenderUnitPanel went too: it re-ran the untabbed RenderSchema and had
    no callers, a second and wrong answer to how a page redraws.

    DECISIONS OVERTURNED, all of them recorded ones:

    - General's "Appearance" section is retired. Master scale and master alpha are
      the addon-as-a-whole exactly as enable, visibility and lock are; two rows whose
      labels both say Master were never a second subject, only the same subject
      broken over a click.
    - The cast bar's "Visibility", "Orientation" and "Sizing and Layout" sections
      fold into one General tab. Visibility held a single row, and one control is a
      drawer rather than a subject; the other two were halves of one question that
      the schema itself already joined, since autoSize decides whether width and
      height are read at all and orientation decides which of the two it overrides.
    - The cast bar's text tabs lose the word "casts" — on a page called Cast bar
      every tab is about a cast — and Text Label's first tab is General, not Label.
    - The label show/text rows keep no alwaysPerUnit flag, so a linked Focus's Text
      Label page renders the note and nothing else, matching Icons and Cast bar.

    TWO LITERALS PROMOTED TO SETTINGS. The charges badge's inset was the bare
    `-2, 2` in modules/IconGrid_Render.lua's SetPoint call. It is
    icons.chargesOffsetX / chargesOffsetY now, defaulted to exactly those numbers so
    a profile that touches neither row draws the badge where it always drew, clamped
    to +/-32 on read because a hand-edited SavedVariable is not an error, it is a
    badge parked off the icon with nothing to say why. Icon:ApplyChargesAnchor
    re-applies both on every config change; the anchor used to be set once at widget
    creation and never again.

    settings/OptionsSetup.lua's degraded stub grows the new members. RefreshScalars
    is NOT cosmetic there: SetAndRefresh calls it on every write now, so a missing
    member is a raise inside `/kcd set`, which still works with no panel. The three
    layout constants that arrived with the tabbed page — BANNER_H, CHROME_GAP, TAB_H
    — deliberately do not appear: options-ui-§8 forbids a host copy of a library
    constant, and tests/test_options_panel.lua scans this file for exactly that.

    Docs follow the panel: settings-panel.md carries the page/tab/count table (also
    pinned by tests/test_schema.lua, so the two cannot drift apart quietly),
    module-map.md and ARCHITECTURE.md the render layer and the message senders,
    common-tasks.md the new recipe step, smoke-tests.md the strip and banner checks.
    README and the remaining docs stop calling the six top-level entries "tabs" now
    that a tab is a thing INSIDE one — they are pages, and the word had to mean one
    thing.

    Gate: 791 passed, 0 failed, 0 skipped; luacheck 0 warnings / 0 errors in 36
    files. Vendor content diff empty for both payloads.

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

commit 8f301465038297fcae581766657a1a1e70109f85
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 1 19:32:01 2026 +0530

    Carry LibKa0s v1.23.0

    Both payloads whole from the v1.23.0 tag — libs/LibKa0s/ into the ship folder,
    tests/_kit/ for the kit — with the CLAUDE.md provenance line in the same commit,
    because tests/test_vendor_sync.lua greps that line for the tag it compares both
    payloads against and has no fallback.

    Options goes 9 -> 13 and OptionsWidgets 8 -> 12. Two of those minors are the
    reason this re-vendor happened at all, and neither existed in the v1.19.0 copy
    this addon was carrying:

    - options-ui-§13, the tabbed page: Helpers.RenderTabbedSchema plus TabStrip,
      SetChromeHeight and the chrome-band internals behind them. A page's rows are
      partitioned by their `group` field, in declaration order, into a pinned strip
      the scroll sits under. The vendored v1.19.0 had RenderSchema and nothing else
      — one long scroll per page was the only shape the library could draw.
    - options-ui-§14, the page banner: Helpers.PageBanner, a control pinned in the
      chrome band ABOVE the strip. It is what lets this addon's Unit picker scope a
      whole tabbed page instead of scrolling away with the rows — a tab click clears
      the scroll, and a picker inside it vanishes on the reader's first click.

    RefreshScalars arrived alongside them and is not cosmetic: Helpers.SetAndRefresh
    ends in it now, so a committed drag tick refreshes values without rebuilding the
    page under the widget being dragged.

    Kit revision 13 -> 14. mock_base's stubFrame tracks enabled state properly
    (SetEnabled/Enable/Disable/IsEnabled) instead of letting the catch-all metatable
    answer IsEnabled with the frame — Blizzard's tab groups mark the selected tab by
    DISABLING it, so without this no suite in any consumer could assert which tab is
    current.

    DebugLog.lua and Pool.lua move on line endings only; the content diff against
    the tag is empty for both payloads.

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

commit 2cb566160c99905922ed964c76bab8f738b3540a
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Aug 27 11:08:26 2026 +0530

    Carry LibKa0s v1.19.0

    This re-vendors both LibKa0s payloads at the v1.19.0 tag: the ship folder
    into libs/LibKa0s/ and the test kit into tests/_kit/. The only file that
    moved is Widgets.lua, which goes to widgets minor 8 and adds ReorderList
    to the widget surface.

    The change is purely additive, so this addon needs no code change to take
    it. Nothing outside libs/ calls ReorderList today; the new surface simply
    becomes available should a future settings panel want it, and the higher
    minor means LibStub will prefer this copy over any older one another
    installed addon vendors.

    The vendored bytes and the CLAUDE.md provenance line move in the same
    commit, which is what the library's release procedure requires: the line
    is what tests/test_vendor_sync.lua reads to decide which tag both payloads
    must match, so a copy without the line, or a line without the copy, is a
    red suite rather than a silent drift.

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

commit f952fc63f2549f5e2bbb6ea93934a662925e4e9c
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 26 19:01:23 2026 +0530

    Re-vendor LibKa0s v1.18.1: the landing logo stops pooling its texture

    OptionsWidgets minor 7 -> 8, and nothing else moves. The library's
    landing page drew its logo with frame:CreateTexture() on an AceGUI
    SimpleGroup -- and a texture is not a widget, so when AceGUI recycled
    that frame the texture rode along and drew again on whatever the page
    used the frame for next. The settings page grew a second logo,
    intermittently, on pool order alone.

    Minor 8 keeps the texture on the frame and reuses it, and hides it when
    the widget is released. No signature, descriptor field or spec key
    moves, so this addon adopts it by carrying the bytes.

    The kit is unchanged at revision 13. The provenance line moves in this
    commit with the payload, which is what tests/test_vendor_sync.lua
    resolves the tag from.

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

commit 55014e0a84319e11c4915d78c180aa64760409ce
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 26 16:59:31 2026 +0530

    Re-vendor LibKa0s v1.18.0 and take the library's resetProfile field

    `afterRestoreAll` wrapping db:ResetProfile() was this addon's own
    spelling of a policy the standard forbids varying (options-ui-§12), and
    eight sibling addons were writing the same lines. LibKa0s-Options-1.0
    minor 9 makes it a descriptor field, so the host names the db and stops
    restating the rule.

    With `resetProfile` supplied the library narrows its own row walk to the
    sessionOnly rows BEFORE consulting skipRestoreAll -- so the veto here is
    belt to that braces on the live path, and the whole policy on the
    degraded one, where there is no library to do the narrowing.

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

commit 853afa6468067b0e7da338f8b22a2508df4137a7
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 26 16:36:52 2026 +0530

    Adopt options-ui-§12: Reset All is a profile reset, with the house wording

    The standard (v2.35.0) settles what the collection's two reset controls
    do and how they ask. Reset All Settings and the Profiles page's Reset
    Profile MUST be the same act -- `db:ResetProfile()` on the ACTIVE
    profile, never a second walk of the schema -- and the confirmation is one
    verbatim, addon-agnostic string.

    Everything the old reset reached by hand lives IN the profile, so all of
    it comes back with one call. `aceDBDefaults()` merges NS.DEFAULT_PROFILE
    over the emptied profile, restoring anchors and every unit's `link`
    flag; `db.profile.spells` goes with the profile and
    Database:OnProfileChanged re-seeds it through BuildSpells on the way
    back, alongside the legacy fold, the spec-key migration and the refresh
    -- the same path a profile switch already takes. ResetAllPositions,
    RestoreUnitLinks and Database:ResetAllSpells all leave this path and
    keep their other callers (`/kcd resetposition`, `/kcd spells resetall`).

    `skipRestoreAll` gains the second clause the standard requires -- the
    Profiles page AND every profile-backed row -- leaving the library's walk
    only the sessionOnly rows a profile reset cannot reach. The degradation
    stub does both halves too: it is there because the LIBRARY is missing,
    not the db.

    AND THE HARNESS WAS PASSING A RESET THAT NEVER RAN. `ResetProfile` was
    one of the no-op DB_STUBS, which was harmless while nothing called it.
    The fake now implements it with the two fidelities that matter: the
    profile table keeps its IDENTITY across a reset (wiped in place, so a
    module holding NS.db.profile from load keeps pointing at the live
    table), and the callbacks fire in BOTH of CallbackHandler's registration
    forms -- core/Database.lua registers all three profile events in the
    string-METHOD form, which a fake that stores the handler and calls it
    cannot dispatch.

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

commit 84bb10f6d821b038faaee22199c51e922d84a6b5
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 26 11:23:38 2026 +0530

    feat(media): new logo art, plus the .png master alongside it

    Replaces the logo with the new collection-wide art and adds the 2000x2000
    .png master the repo was missing, so the runtime .tga can be regenerated
    here rather than from wherever the render came from.

    The .tga is Lanczos-downscaled at 300x300 — MAIN_LOGO_SIZE in
    settings/Panel.lua, and the size the landing page draws it at, so it stays
    pixel-exact.

    .pkgmeta now excludes media/logos/*.png and *.jpg. Only the .tga is loadable
    by the client; the other two are project-page and regeneration sources.
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01YPd19uJ5DWdV4mKquWCSYZ

commit 066f694f391e981991e44cdbc6ea1d0bc242cbf0
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Aug 25 17:45:18 2026 +0530

    docs: catch the inventory and the badge up to the pool cases

    The LibKa0s-Pool-1.0 adoption added three cases to test_icongrid_buildlist.lua
    and neither the generated inventory nor the README badge moved with them, so
    both have been reading 780 against a suite of 783 since acf4a82. Regenerated
    docs/test-cases.md with `lua tests/run.lua --list` and took the badge to
    783/783 in the same change, which is the lockstep the pair is supposed to keep.

    Also corrects a count the adoption left behind: core/PoolSetup.lua's degraded
    arm republishes FOUR keyed members, not three — CountsKeyed is in the fallback
    table beside NewKeyed, AcquireKeyed and ReleaseAllKeyed. docs/ARCHITECTURE.md
    said three in both places it describes the seam, and docs/module-map.md in the
    one place it does; all three now say four. Note that nothing in this addon
    calls CountsKeyed today, which is worth a decision but not a silent deletion.

    luacheck 0/0 in 36 files, suite 783/783. Both vendor diffs read as expected:
    content empty, bytes differing on line endings only.

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

commit fc3be836ae4c2481176125cba22660455739d0d4
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Aug 25 17:30:11 2026 +0530

    Re-vendor LibKa0s v1.17.0

    Pool minor 3 makes ReleaseAll park the active set backward so a position gets its own
    object back. It cannot reach this addon: IconGrid is the collection's only KEYED
    consumer, and where the key is the mapping there is no position to preserve. Minor 3
    says so explicitly now — ReleaseAllKeyed's order is documented as carrying no meaning.

    No adoption change; the re-vendor keeps the payload consistent across the collection.

    luacheck 0/0 in 36 files, suite 783/783.

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

commit cef35d4beb0b8eeb4e977a3efd11d5056a369895
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Aug 25 16:05:28 2026 +0530

    docs: correct the adopted-major count and the test-kit doc revision

    Comment-only corrections found by the doc sync; no behavior changes. Lint and the full suite pass unchanged.

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

commit 8bc4ee115581dbf9127571498aa9db64867676d9
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Aug 25 15:36:56 2026 +0530

    docs: catch the docs up to the LibKa0s-Pool-1.0 adoption

    acf4a82 added core/PoolSetup.lua and its TOC load line without any doc
    following it, so the pages that count seams had gone stale.

    - CLAUDE.md: seven adopted LibKa0s modules -> eight, with core/PoolSetup.lua
      added to the one-setup-file-each list.
    - docs/ARCHITECTURE.md: Pool added to the adopted-majors and setup-file lists;
      the degradation clause now names all three seams that report no absence
      (Env, Media, Pool) instead of Env alone; core/PoolSetup.lua inserted into
      the load order at its real TOC position and the rest renumbered;
      perf-analysis/README.md moved to the verification-and-record table; and
      docs/revendor/ named in the frozen-directory list so its two files are no
      longer unregistered.
    - docs/module-map.md: EnvSetup, PoolSetup and MediaSetup added to the
      directory tree (all three were missing); a TOC load-order entry for
      PoolSetup; and LibKa0s added to the external-dependency roll-call with its
      eight adopted majors.
    - docs/testing.md: the surface-parity seam list gains Perf, matching the five
      cases tests/test_surface_parity.lua actually carries.

    Documentation only - no .lua/.xml/.toc/.luacheckrc/.pkgmeta byte touched.

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

commit acf4a82dbfe8784a7b0b5b0de2d3c8c8a3e0fc67
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Aug 25 14:30:07 2026 +0530

    Adopt LibKa0s-Pool-1.0 for the icon-grid pool

    The adoption waited for Pool minor 2, and the wait was not preference. `active`
    here is a map keyed by spellID, and the keying is load-bearing: it is the O(1)
    index the SPELL_STATE fan-out uses on every cooldown-state message, and it is
    what makes the one-widget-per-spellID invariant enforceable. Minor 1's `active`
    was an array.

    Porting to minor 1 anyway would have been worse than not porting. Its ReleaseAll
    walks `for i = 1, #active`, which over a keyed table iterates nothing, so every
    icon would be hidden, none freed, and the factory called forever — the exact leak
    the library exists to end, reached through its own documented API, with the grid
    still drawing and the suite still green. LibKa0s#13; minor 2 added the keyed
    members and made ReleaseAll raise on a keyed pool.

    What stays host code is everything about an ICON rather than a pool: the five
    stamped fields, the cooldown-text ticker unregistration, the cooldown clear and
    the glow stop. One behavior improved — AcquireKeyed on a live spellID returns the
    button already there, where the old code took a second from the free list and
    orphaned the first.

    Two characterization tests added, because the failure this prevents is invisible
    to every other case in the file: a release/rebuild cycle must allocate nothing,
    and every widget drawing afterwards must be one that already existed. Both would
    pass a pool that leaked before they were written; neither passes one now.

    Closes #13.

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

commit 39e259588aff3c0a75c1e8e991e8b4f1c9f668f4
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Aug 25 14:26:32 2026 +0530

    Re-vendor LibKa0s v1.16.0

    Both payloads whole, from the tag, with the CLAUDE.md provenance line in the same
    commit — tests/test_vendor_sync.lua reads that line to pick the tag it compares
    against, so a split would fail the gate.

    Pool minor 2 (keyed pools, and ReleaseAll now raises on one instead of silently
    recycling nothing), Widgets minor 7 (two optional CopyWindow fields, both absent
    by default), DebugLog minor 12 (its copy window is Widgets.CopyWindow now, with a
    hard NEEDS_WIDGETS floor), kit revision 13 (CreateFrame records its arguments, so
    a suite can ask what a frame was named).

    Nothing in this addon's own code changes. Lint and the headless suite green.

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

commit 0c668b06b13c6006aee3f19149c7e92c37285d6d
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Aug 25 12:32:37 2026 +0530

    Record the 2026-08-25 LibKa0s re-vendor

    The re-vendor found an empty delta: this addon was already carrying LibKa0s
    v1.15.0, every shipped file at the tag's minor, both payloads byte-identical to
    `git archive v1.15.0`, and test kit revision 12 paired as the rule requires. No
    bytes were copied and the provenance line did not move.

    The bundle is the record of that. 01_DELTA.md carries all five reads with the
    command that produced each, including the consumption map and the majors this
    addon does not consume; 05_SUMMARY.md carries the gate results that confirm it
    independently — the addon's own tests/test_vendor_sync.lua passes both its
    assertions inside a full green run.

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

commit f813774d10b2d65c7706ea4c2f1e605562498d4d
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Aug 25 11:03:34 2026 +0530

    docs: record the fast green gate — 5.66s over 780 cases, badge to 780

    The testkit revision 12 that arrived with the LibKa0s v1.15.0 re-vendor
    (9907d8d) carried a compiled-chunk cache in the loader, and this is the run that
    records what it bought here. tests: 5.66s wall, 29% CPU, 780 passed / 0 failed.
    The previous recorded run in this repo, 20260807-114618, took 45.2s over 756
    cases. Nothing in tests/ changed for it; the runner is the same file.

    --jobs stays off. tests/run.lua passes Kit.run no `jobs` opt, so the suite runs
    serially, and at 5.66s it is under the ~10s threshold testing-§14 sets for
    reaching for sharding at all. Sharding a suite this size buys process startup
    and costs a readable failure ordering.

    THE BADGE WAS THE ONLY DRIFT. docs/test-cases.md regenerates byte-identical
    against the suite, and its Totals row already read 780; the README [Tests] badge
    was still at 774, which is exactly the silent-staleness the badge rule exists
    for. Both now say 780, and `diff <(lua tests/run.lua --list) docs/test-cases.md`
    is empty.

    Three file:line citations in DEPENDENCIES.md were repointed at the same time,
    all of them moved by the revision-12 copy or by ordinary drift, none of them
    resolving any more: tests/_kit/loader.lua:31 -> :72 (the setfenv call in
    Loader.load, pushed down by the chunk cache), tests/test_coresetup.lua:213 ->
    :214, and docs/testing.md:100 -> :107 for the `file -b` troubleshooting line.
    docs/ARCHITECTURE.md's compat-layer.md trigger row said core/Compat.lua is 490
    lines; it is 496.

    Vendor diffs: tests/_kit is clean against ../LibKa0s/testkit. libs/LibKa0s
    differs only in DebugLog.lua, by four comment lines added to the library's
    working tree after v1.15.0 — this repo vendors the tag, which is what
    tests/test_vendor_sync.lua compares against, so the suite is green on it.

    Gate: 780 passed, 0 failed, 0 skipped; luacheck 0 warnings / 0 errors in 35 files.

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

commit 343f430c58f8f34e4327a4419e67171fafc989bf
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Aug 25 04:12:29 2026 +0530

    refactor(env): read the TOC through LibKa0s-Env-1.0

    Three inline C_AddOns ladders, none of them in core/Compat.lua — which is why an
    audit of the shim files counted six copies across the collection when there were
    eleven. A test now fails if a fourth appears.

    core/EnvSetup.lua publishes NS.Meta(field) and NS.Version(); core/KickCD.lua's
    `version` verb, settings/Slash.lua's NS.Slash.Version and core/PerfSetup.lua's
    descriptor now resolve the same function instead of three copies free to drift.
    The seam stops at C_AddOns rather than reaching for the deprecated bare global
    the reference seam carries, because none of the three copies did either
    (architecture-§1) — a degraded install gets exactly what it got before.

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

commit 9907d8dc0755afbd1cc72c4d28d0e5150b003696
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Aug 25 03:49:31 2026 +0530

    chore(libs): re-vendor LibKa0s v1.15.0, and take testkit revision 12 with it

    Adds LibKa0s-Env-1.0, LibKa0s-Pool-1.0 and LibKa0s-Item-1.0 to the payload,
    takes Widgets to its own file and DebugLog to the 1500-line console buffer.
    Nothing in this addon adopts the new majors yet — this commit is the copy and
    the provenance line.

    tests/run.lua needs no edit for the three new files: this runner derives its
    library load list from libs/LibKa0s/LibKa0s.xml via Loader.xmlFiles rather than
    hand-listing it, so Env, Pool and Item are picked up in XML order for free.

    THE TEST KIT MOVED, and it is a debt, not new scope. LibKa0s' testkit/ is
    untouched by this release; the kit went to revision 12 back at v1.14.0
    ("testkit revision 12: the green gate stops waiting on the disk", 2c3a39c) and
    this addon never took it. It has to move now because tests/_kit/vendor_sync.lua
    registers two cases — one for libs/LibKa0s, one for tests/_kit — and both are
    compared against the single tag bundledVersion() greps out of CLAUDE.md. One
    provenance line governs both payloads, so rolling the line to v1.15.0 obliges
    the kit to move with it; there is no green state otherwise. Revision 12 is
    backward compatible (a compiled-chunk cache in the loader, an opt-in
    --jobs/--shard defaulting to 1), so nothing in tests/ changes for it and
    docs/test-cases.md regenerates byte-identical.

    The one behaviour change is the console history depth, which is the library's
    and arrives with the copy. The three live places that stated the old 500 —
    core/DebugLogSetup.lua:5, docs/module-map.md:232 and docs/smoke-tests.md:609,617
    — move to 1500 with it. The frozen docs/audits/ and docs/automated-tests/
    bundles keep their 500s: they are records of what was true when they were cut.

    diff -r against the source repo is empty for both payloads.
    Gate: 774 passed, 0 failed, 0 skipped; luacheck 0 warnings / 0 errors.

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

commit 0ed27c1e2a8407e1cd0a0ff7b89cf8dcf41f17d4
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Mon Aug 24 00:48:50 2026 +0530

    Adopt LibKa0s-Media: shared art on the windows, one copy of the font

    The debug console's title bar drew the words Copy and Clear beside a
    multiplication sign, and the perf panel closed with the same sign. All three
    controls are LibKa0s' to draw, and since v1.10.x the library draws this
    collection's own marks instead — but only when it is told which addon folder to
    build a texture path from. It is vendored: there is no one path to it, and a copy
    cannot know which folder it was copied into. So this is the wiring.

    TWO SEAMS NOW SAY WHO IS ASKING. core/CoreSetup.lua wraps MakeCloseButton to
    supply the folder name, once, so every close control this addon builds gets the
    same mark rather than each call site remembering; core/DebugLogSetup.lua passes
    `addonName` in the console descriptor, beside `name` and explicitly not instead
    of it. They are the same string here and different questions everywhere — `name`
    seeds the frame globals, `addonName` is a texture path — and a host where the two
    diverge hands the library a path into nowhere, which draws nothing and raises
    nothing.

    core/PerfSetup.lua was the live version of that hazard. It called
    NS.DebugLog.MakeCloseButton(frame, Hide) — two arguments onto what is two
    arguments in the copy we shipped yesterday and three in the one we ship today.
    Correct, green, and silently wrong the moment the payload moved: the panel would
    have kept its multiplication sign while the console beside it drew art, with
    nothing to report it. It is the only window in this addon whose close button the
    host builds, so there was no second surface to notice on. It goes through the one
    wrapper now, and a case walks the TOC to prove nothing else reaches around it.

    THE FONT STOPPED BEING OURS. media/fonts/ held a copy of JetBrains Mono and its
    OFL license, and five sibling addons held the same two files. Two copies of a
    face is two licenses to track and two provenance stories, and a collection whose
    addons stop looking like one author's work the first time one copy is regenerated
    and the other is not. The face ships inside the library payload now, at
    libs/LibKa0s/media/fonts/, and core/MediaSetup.lua resolves it. Its TOC line sits
    above core/Constants.lua and says so in a comment: Const.FONT_MONO is resolved at
    file load, so the seam has to be published first. The fallback is the client's own
    STANDARD_TEXT_FONT and never a constructed path — SetFont accepts a path to a file
    that is not there, fails to load it, and the text simply does not draw.

    The LibSharedMedia registration moved with the bytes rather than being deleted. It
    used to sit above the DebugLog guard in core/DebugLogSetup.lua on the argument that
    exposing the face to other addons is not the console's to skip just because the
    console is missing. That argument still holds; it just is not the console's job,
    it is the media seam's, and it is guarded on the library that owns the bytes.

    Bundles LibKa0s v1.10.2: libs/LibKa0s/ and tests/_kit/ (revision 11) re-vendored,
    provenance line moved with them. The brief named v1.10.1; the library's own tag
    had moved to v1.10.2 — the perf panel's no-`decorate` path had the same dropped
    argument one window over — and the vendored gate compares the line against the
    payload, so the line follows the bytes.

    The settings panel is deliberately untouched. Its widgets are LibKa0s-Options-1.0's
    and iconifying them is a library change with a collection-wide blast radius,
    recorded as an open evolution upstream. That includes the Spells page's own row
    buttons, which still draw Blizzard chat-scroll arrows and the transmog-remove
    atlas: the catalog carries arrow-thick-up, arrow-thick-down and ban, and swapping
    them is a real improvement, but it is a settings-panel change and belongs with the
    rest of that decision rather than smuggled in here.

    Three test groups pin the ARGUMENT, not the appearance, because appearance is the
    one thing that cannot fail loudly here: tests/test_mediasetup.lua for the seam and
    the catalog cross-check, a CoreSetup case asserting the wrapper passes "KickCD" as
    the third argument, and a DebugLogSetup case grepping the descriptor for
    `addonName = addonName` beside `name`. What is drawn is checked by hand, so
    docs/smoke-tests.md gains section 26 — what the three controls should look like,
    that the console close and the perf panel close must match, and that a
    multiplication sign means the folder name stopped being passed.

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

commit 26cb3c635e33d96068ed2ffd104c3bb4420e16c9
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 19 20:33:50 2026 +0530

    fix(icons): classify GCD vs real cooldown by total length, not remaining

    Reported from the field: the icon read "ready" a second or two before the
    interrupt actually came off cooldown — small, but it is exactly the window
    the addon exists to show.

    The alpha, tint and swipe-suppression curves all stepped at GCD_UPPER (1.6s)
    against :EvaluateRemainingDuration, so the final 1.6s of a real 15s cooldown
    was numerically indistinguishable from a 1.5s GCD lockout and rendered the
    same: ready alpha, no tint, and — with suppressGCDSwipe on by default — no
    swipe and no countdown either.

    :EvaluateTotalDuration asks the question the curves were always asking. A GCD
    totals ~1.5s for its whole life and a real cooldown totals 15s+ for its whole
    life, so the classification is made once and never drifts as the cooldown
    winds down. Same C-side path, same secret-value safety; a client without the
    method falls back to the old approximation rather than erroring, since
    docs/midnight-quirks.md records that surface as a measured snapshot.

    docs/smoke-tests.md had written the defect down as an expectation ("the icon
    brightens ... in the final ~1.6s"); corrected along with the data-flow,
    common-tasks, message-bus and testing prose.

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

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

    docs(perf): record the first in-game capture — 20260807-131311

    KickCD 1.2.1, solo world combat in Falconwing Square, both arms
    combat-gated with no /reload between them.

    The frame-time delta is -0.17 ms/frame: inside the resolution floor
    AND sign-backwards, so it is unresolved, not null. The buckets are the
    reading. Accounted cost is a RANGE — 6.30 ms/s if the declared nesting
    holds, 12.99 ms/s if it does not — because every Perf.Note call site
    passes two arguments, so containment is declared but never observed.

    Closing that gap is the analysis' first action. This bundle also makes
    core/PerfSetup.lua's nesting decisions citable against a committed
    record instead of the early uncommitted capture they were taken off.

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

commit 2d054919937ce9972afa23af0b42df3b7b87d7f9
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Aug 7 14:01:26 2026 +0530

    docs(perf): replace docs/perf-runs/ with the frozen docs/perf-analysis/ bundle store

    An in-game capture and the reading taken off it used to live apart: the JSON
    record went into `docs/perf-runs/` as a flat `<YYYY-MM-DD>-ingame-<label>.json`,
    and whatever conclusion it supported was written wherever the decision was
    made. Nothing held the two together, so a record could outlive its
    interpretation or drift from it silently, and `performance-§8`'s rule that an
    interpretation without its record is an assertion had no structure enforcing it.

    Captures now land as one frozen dated bundle per capture at
    `docs/perf-analysis/<YYYYMMDD-HHMMSS>/`, carrying exactly three files:
    `report.md` (what the client printed, plus the run's lifecycle lines),
    `dump.json` (the record verbatim, one line, never re-encoded) and `ANALYSIS.md`
    (the write-up). The stamp is local time rendered from the record's own
    `timestamp`, so a capture analysed a week later still sorts where it happened.
    A committed capture with no reading beside it is now an anti-pattern in its own
    right.

    The store is still empty here — no `/kcd perf` capture has been committed yet —
    and the README says so plainly rather than reading as though records exist.

    The rewritten README also states something a reader needs BEFORE reading a
    KickCD report: all eleven `Perf.Note` call sites across the eight bracketed
    buckets pass two arguments and no `parentKey`, so `observedWithin` is never
    populated and the three nesting claims declared in `core/PerfSetup.lua:93-102`
    print as "declares itself within X — not observed". They are reasoned, not
    measured, and an `ANALYSIS.md` must not subtract a declared child from its
    declared parent as though the overlap were confirmed. Threading `parentKey`
    through those call sites is named as a legitimate follow-up.

    Documentation only, plus one comment in `core/PerfSetup.lua` that named the old
    path. No behaviour changes and no code changes.

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

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

    automated-tests: record run 20260807-114618 — green

    Four-suite run through the vendored kit (LibKa0s v1.8.2, test-kit
    revision 10). lint 0 warnings / 0 errors over 33 files; tests
    756 passed, 0 failed, 0 skipped; perf 5 scenarios;
    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 56e80892ce90640d0a16cbd232b7c142aef49ea2
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Aug 7 11:37:49 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 710e32936a3d59c936033be1b0867ba72788e906
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Aug 7 11:08:30 2026 +0530

    Re-vendor LibKa0s v1.8.2; test kit to revision 10

    Both payloads move together from the annotated tag v1.8.2 (ac857fd), and
    CLAUDE.md's provenance line moves in the same commit — which is what
    tests/test_vendor_sync.lua reads and resolves. The previous attempt failed
    exactly there: it vendored from untagged master while the line still said
    v1.8.1.

    What the new bytes buy is a runner that stops littering. Everything
    run-automated-tests.sh writes goes down a plain shell redirect, and a redirect
    bypasses git's clean/smudge filters entirely, so in this CRLF-pinned repo every
    bundle landed LF on disk while .gitattributes said CRLF. git status never
    mentioned it and `git add --renormalize` never fixed it, because the index was
    already right and only the working tree was wrong. Revision 10 adds one pass at
    the end of the run that asks `git check-attr text eol` per path and converts
    accordingly, skipping any path whose text is unset so a binary-marked asset is
    left alone. It runs once over the whole bundle rather than at each write site
    because perf.json is written by tests/perf.lua through --out, not by the runner.

    libs/LibKa0s/Options.lua carries a comment citation fix (options-ui-§41 ->
    options-ui-§1); Kit.VERSION goes 9 -> 10.

    Run 20260807-110522 is the proof, and it is the first bundle here written by the
    fixed runner: all seven files CRLF on disk, CR count equal to LF count. Lint,
    tests and complexity artifacts are byte-identical to 20260807-022824 — a
    re-vendor should move nothing else, and it did not.

      lint        pass  0 warnings / 0 errors in 33 files
      tests       pass  756 passed, 0 failed, 756 total
      perf        pass  5 scenarios (recorded, non-gating)
      complexity  pass  0 warnings, 15533 NLOC / 2059 funcs, max CCN 15

    test_vendor_sync's two cases pass. Working-tree strays measured 0 before and 0
    after, by line-endings-§7's corrected check.

commit 76726f03a5a0a52c58ef45c8e3da5f2eb624ac03
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Aug 7 03:17:06 2026 +0530

    docs: restore the canonical Standards-compliance wording in CLAUDE.md

    The `## Standards compliance (read first)` section had drifted from
    documentation-§6 item 3 in two ways that matter to how an agent behaves here:

    - Development was "measured against" the standard, where the canonical wording
      grades it a MUST — a softened MUST reads as advisory.
    - The accepted-deviation arm sent the reader to "a justifying comment", but the
      register moved: documentation-§3 makes `docs/ARCHITECTURE.md` →
      `## Documented deviations` the single home, and a deviation not in that table
      is not ratified. The upstream arm now names WowAddonStandards explicitly.

    Substance taken from the fetched documentation.md (standard v2.28.0, 2026-08-06);
    the section's own voice, bullets and surrounding prose are unchanged.

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

commit f84dc07efcf6c4767d07ee4c5613342d96eba846
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Aug 7 02:36:06 2026 +0530

    docs: correct six stale comment citations; record run 20260807-022824

    Two authorised fixes.

    Comment-citation corrections (comment-only; no line that runs was
    touched). Each named a path or member that resolves to nothing today,
    and each has exactly one determinable successor:

      core/Constants.lua:94          modules/DebugLog.lua -> core/DebugLogSetup.lua
      defaults/Profile.lua:9         Database:InitDB -> Database:Init
      modules/IconGrid_Layout.lua:9  core/IconGrid.lua -> modules/IconGrid.lua
      modules/IconGrid_Layout.lua:18 core/IconGrid.lua -> modules/IconGrid.lua
      modules/IconGrid_Render.lua:875 core/IconGrid.lua -> modules/IconGrid.lua
      tests/wow_mock.lua:709         tests/loader.lua -> tests/_kit/loader.lua

    Citations that read as deliberate history were left alone: a path in a
    past-tense sentence ("this replaces modules/DebugLog.lua", "used to be
    Castbar:ApplyConfig", "Const.PANEL_PADDING_X is deleted outright") is
    correct as written and rewriting it would falsify the record.

    Automated-test record. The four-suite run was stale against the tree;
    20260807-022824 replaces it, green, with no suite skipped for the first
    time in this record. tests/perf.lua now exists, so perf reads pass over
    5 scenarios where every earlier row reads skip, and RESULTS.md's Perf
    section is rewritten rather than refreshed - it asserted the addon ships
    no scenarios, which is no longer true. Tests 737 -> 756, lint 32 -> 33
    files, complexity averages flat (avg CCN 2.1, avg NLOC 6.5) against
    +103 NLOC, zero CCN warnings held. Band steady at four files; only
    tests/wow_mock.lua grew (+35, tracked as KCD-30).

commit f658a2583b7e32c0d6399ae30750188c3908639e
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Aug 7 02:09:50 2026 +0530

    docs: repoint DEPENDENCIES.md's stale file:line evidence

    Four citations no longer resolved, which is the specific failure that makes
    the toolchain list stop being trusted:

    - the setfenv sandbox moved with the kit (tests/loader.lua:84 ->
      tests/_kit/loader.lua:31); the old path does not exist at all
    - tests/test_coresetup.lua's io.popen("ls ...") is at :213, not :169
    - docs/testing.md's `file -b` troubleshooting note is at :100, not :67
    - tests/test_vendor_sync.lua is five lines now, not four

    Evidence only; no entry added, removed or reworded.

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

commit f95c0d63de21b1267db961dd06960e2558d8298e
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Aug 6 21:21:28 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 90f9498dd912cca2b31c927e8551fd7a01142c3a
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Aug 6 18:41:08 2026 +0530

    Move the LibKa0s provenance line into CLAUDE.md; test kit to revision 9

    Adopts LibKa0s v1.8.1, which is the test kit alone — no shipped library file
    moved, so libs/LibKa0s/ is untouched and still byte-identical to the tag.

    tests/_kit/ re-vendored from v1.8.1. Kit revision 9 changes exactly one file:
    vendor_sync.lua now reads the "Bundles [LibKa0s](…) vX.Y.Z (MIT)." line out of
    the consuming repo's CLAUDE.md instead of its README.md, via a new
    provenanceFile opt. There is no fallback, deliberately — a repo that
    re-vendored without moving its line fails the case naming CLAUDE.md, rather
    than sitting half-migrated with two lines that can disagree.

    So the line moves here in the same commit as the bytes it describes. It
    answers "which LibKa0s does this build carry?", which is a maintainer's
    question; CLAUDE.md is where this repo already keeps the facts a maintainer or
    an agent needs about the build. README.md is the page a player reads, and a
    vendored-library inventory was never something a player needed — so the whole
    "Libraries and credits" section comes out, LibKa0s sentence, Ace3 inventory and
    all. Nothing in it was an external credit, so no Credits section survives it.

    Also unlinks the Standard badge in README.md; the badge markdown is unchanged.

    docs/test-cases.md regenerated: revision 9 renames one case ("…the README says"
    → "…CLAUDE.md says"). Counts do not move — still 756/756 — so the README
    [Tests] badge stays as it is.

    docs/pending/LEDGER.md: LIBKA0S-03 recorded the provenance line as living in
    README.md; noted as superseded rather than rewritten. DEPENDENCIES.md: the git
    and diff rows pointed at line numbers inside the old hand-copied gate and at
    README.md as the file carrying the tag; both now point at
    tests/_kit/vendor_sync.lua and CLAUDE.md.

    lua tests/run.lua: 756 passed, 0 failed, 0 skipped, 756 total
    luacheck .: 0 warnings / 0 errors in 33 files

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

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

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

    This repo is client-bound, so the working tree is pinned CRLF on every
    platform: `* text=auto eol=crlf`, the `*.sh text eol=lf` carve-out that
    keeps the vendored test runner executable, and the shared binary block.
    The body after the pin is byte-identical to every other repo in the
    collection, so diffing a client-bound repo against a non-client one shows
    one decision rather than two documents.

    Working-tree stragglers written by tools that bypass git's filters were
    re-materialized from the index; the headless suite passes unchanged.
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01A9nTGYaHsqCEf6obganps6

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

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

    Renamed (git mv, history preserved): saved-variables.md -> schema.md, the
    canonical name for the persisted shape across the collection.

    Created: common-tasks.md — the last missing Tier 1 doc — which absorbs the
    retired conventions.md as its "House rules" half and adds the recipes those
    rules govern: add a setting, a slash command, a tracked spell, a locale
    string, a message. The spell recipe states the trap that cost issue #8: spec
    keys are Blizzard's NUMERIC ids written through Const.SPEC aliases, because
    uppercased spec NAMES made a frFR Elemental Shaman derive "ELEMENTAIRE",
    miss the "ELEMENTAL" key and track nothing at all.

    Created: profiles.md — the Tier 2 trigger fires here, since AceDB profiles are
    user-visible through the Profiles settings page. It records the three
    deliberate choices in settings/Profiles.lua (no Defaults button, rendered into
    our container rather than its own window, re-opened on every OnShow so the
    page reflects the CURRENT profile after a switch) and the OnProfileReset
    signature detail: AceDB passes nil as the third argument there, so the shared
    handler substitutes the active key rather than trusting the parameter.

    Retired: conventions.md.

    ARCHITECTURE.md gains `## Documentation map`. Six of seven Tier 2 docs are
    present here — the most in the collection — and debug.md is the one recorded
    as "Not applicable", since /kcd debug's subcommands dump state through the
    library console rather than adding a surface of their own.

    Verified: 756 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 f3335df37ab7aab31b95c42a31c28c885839ba09
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Aug 6 01:11:56 2026 +0530

    docs: retire the two claims this changeset made false

    Two facts moved under the remediation branch and their doc footprint did not
    follow.

    README `[Tests]` badge: 747/747 -> 756/756. The suite gained cases across
    M3-13, M3-12 and M4-02 (the panel-layout metrics case, the live-bucket castTick
    count, and the malformed-profile render comparison). `docs/test-cases.md` is the
    authoritative count and already says 756; the badge is static and goes stale
    silently, which is exactly why CLAUDE.md requires the two move in the same
    change (documentation-§1 / testing-§5).

    `docs/ARCHITECTURE.md` message-bus invariant: M4-04 gave CONFIG_CHANGED and
    PROFILE_CHANGED one named emitter each, so the paragraph describing
    CONFIG_CHANGED as "emitted from several modules" and carrying PROFILE_CHANGED's
    two-emitter split as a recorded intentional deviation now describes code that no
    longer exists — and that deviation is no longer in the `## Documented
    deviations` register, so the paragraph was the last place claiming it. Replaced
    with what the code does: one owning module per message,
    `Helpers.FireConfigChanged` and `fireProfileChanged` as the single send sites,
    and SPELL_STATE (3) / GRID_LAYOUT (2) keeping several sites inside their own
    owning module, which is what architecture-§4 asks for. This now agrees with
    `docs/message-bus.md`, which was already correct.

    Verified after the edit: 756 passed, 0 failed, 0 skipped; luacheck 0 warnings /
    0 errors over 33 files; lizard clean, no threshold exceeded; both vendored-copy
    gates (`libs/LibKa0s`, `tests/_kit`) empty on the content AND the byte reading
    against LibKa0s v1.8.0.

    Not changed, deliberately: `docs/automated-tests/RESULTS.md` still reports the
    `20260804-233245` run at 737/737 over 32 files. It is a generated record whose
    checkpoint is the release, not this commit — `/wow-addon:bump-version` refreshes
    it.

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

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

    docs: retire the two claims this changeset made false

    Two facts moved under the remediation branch and their doc footprint did not
    follow.

    README `[Tests]` badge: 747/747 -> 756/756. The suite gained cases across
    M3-13, M3-12 and M4-02 (the panel-layout metrics case, the live-bucket castTick
    count, and the malformed-profile render comparison). `docs/test-cases.md` is the
    authoritative count and already says 756; the badge is static and goes stale
    silently, which is exactly why CLAUDE.md requires the two move in the same
    change (documentation-§1 / testing-§5).

    `docs/ARCHITECTURE.md` message-bus invariant: M4-04 gave CONFIG_CHANGED and
    PROFILE_CHANGED one named emitter each, so the paragraph describing
    CONFIG_CHANGED as "emitted from several modules" and carrying PROFILE_CHANGED's
    two-emitter split as a recorded intentional deviation now describes code that no
    longer exists — and that deviation is no longer in the `## Documented
    deviations` register, so the paragraph was the last place claiming it. Replaced
    with what the code does: one owning module per message,
    `Helpers.FireConfigChanged` and `fireProfileChanged` as the single send sites,
    and SPELL_STATE (3) / GRID_LAYOUT (2) keeping several sites inside their own
    owning module, which is what architecture-§4 asks for. This now agrees with
    `docs/message-bus.md`, which was already correct.

    Verified after the edit: 756 passed, 0 failed, 0 skipped; luacheck 0 warnings /
    0 errors over 33 files; lizard clean, no threshold exceeded; both vendored-copy
    gates (`libs/LibKa0s`, `tests/_kit`) empty on the content AND the byte reading
    against LibKa0s v1.8.0.

    Not changed, deliberately: `docs/automated-tests/RESULTS.md` still reports the
    `20260804-233245` run at 737/737 over 32 files. It is a generated record whose
    checkpoint is the release, not this commit — `/wow-addon:bump-version` refreshes
    it.

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

commit 1d58780df9b420905a75b7a67781bba752502289
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 23:27:04 2026 +0530

    test(castbar): render a malformed profile and compare it against a fresh one  [fixes M4-02 test]

    The old case asserted table IDENTITY — rawequal(Castbar.INT_FALLBACK,
    NS.CASTBAR_DEFAULT.interruptible) — between the two locals the commit had just
    rewired and the defaults sub-tree. That is stricter than the rule it cites.
    savedvariables-§2 asks for one DECLARATION site, and
    `copy(NS.CASTBAR_DEFAULT.interruptible)` is one declaration site, renders
    identically, and is the correct shape as soon as any consumer needs to write to
    the sub-config — identity reddens on it. Meanwhile the requirement the case
    states in its own comment ("a malformed profile that falls back must render
    exactly like a fresh one") was never driven: the sibling cases read the fallback
    tables directly, so stateConfig's fallback arm was never taken while a frame was
    observed.

    Now: the declaration-site case compares scalar fields against the shipped
    defaults instead of demanding identity, and a new case drives the requirement.
    It renders a cast on an enabled instance with a fresh profile, snapshots both
    bars' status-bar colors, the name text color, both border alphas and both
    backdrop border colors, then sets units.target.castbar.interruptible /
    .uninterruptible to each malformed shape stateConfig exists for (key absent, and
    a bare color string under the key) and asserts the render is slot-for-slot
    identical.

    PRODUCTION MUTATION used to prove red: re-forked INT_FALLBACK / UNINT_FALLBACK
    in modules/Castbar.lua as a hardcoded second copy drifting on the interruptible
    border (borderShow false, borderSize 1) — the exact historical bug. RED: "a
    profile whose per-state config is absent must render identically to a fresh one
    (slot 13) (expected 1, got 0)".

    Also verified the new pair does NOT redden on correct code the old one did:
    replacing the shared references with a shallow copy of the defaults keeps both
    cases GREEN at 756 passed. Reverted: 756 passed, 0 failed.

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

commit f78e3a16f936552bc3f253033930c3b5357d2e9b
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 23:19:56 2026 +0530

    test(perf): count castTick's exits off the live bucket, not off the source text  [fixes M3-12 test]

    The leak case asserted a line-based source scan over four modules: inside any
    function that opens `__t0 = Perf.on`, every `return` must be immediately
    preceded by a `Perf.Note(` line. Its target was literally the two lines the same
    commit had just added, and its written justification — "behavioral coverage is
    not available here… the harness drives neither" — was false.
    tests/test_castbar_frame.lua:251-259 already drives exactly the leaked exit
    (`inst.current = nil; inst.frame:_run("OnUpdate")`), and the case immediately
    above at test_perfsetup.lua:551 already reads live counts through P.__buckets()
    and asserts `b.calls == 5`. The right shape was already in this file.

    Being line-based, the scan also reddens correct code: a `return` inside a nested
    closure, or a bracket closed through a helper rather than a literal
    `Perf.Note(`, is reported as a leak.

    New case mirrors the PollSpell one. With P.on, it starts a cast on a real
    Castbar instance, runs three ordinary `inst.frame:_run("OnUpdate")` frames,
    clears inst.current and runs the teardown frame, then asserts
    `buckets.castTick.calls` equals the total number of OnUpdate invocations — one
    short is exactly what an unclosed guard exit produces. The scan is KEPT as a
    supplementary lint for brackets nothing yet drives (cdText, behind a C_Timer
    ticker); its comment now says that instead of the false claim.

    PRODUCTION MUTATION used to prove red: deleted
    `if __t0 then Perf.Note("castTick", debugprofilestop() - __t0) end` from
    onUpdate's no-duration guard exit, modules/Castbar.lua:692. RED:
    "every OnUpdate exit must be counted; got 3 of 4 — one short means the teardown
    frame leaked (expected 4, got 3)". Reverted: 755 passed, 0 failed.

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

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

    test(constants): assert the panel layout metrics arrive as positive numbers  [fixes M3-13 test]

    The commit that deleted the host copies of PADDING_X and ROW_VSPACER also
    deleted the coverage that guarded them: PANEL_PADDING_X left the "every panel
    metric is a positive number" loop, and test_surface_parity.lua moved ROW_VSPACER
    into the live-only exclusion list. What replaced it was a source scan asserting
    the host does not RESTATE a numeric literal — a correct lint, but it stays green
    if the library stops publishing the value at all.

    That hole is live. settings/Panel_Render.lua:20 binds
    `local ROW_VSPACER = Helpers.ROW_VSPACER` AT FILE LOAD and forwards that binding
    at :83 and :117; settings/Panel_Widgets.lua:49 does the same. A nil arriving
    there is silent: AddSpacer creates a full-width SimpleGroup with no height, every
    options row loses its spacing in game, and the whole suite stays green.

    Two cases restore the guarantee, read off the instance rather than off Const.
    The first asserts LibKa0s-Options publishes PADDING_X, ROW_VSPACER,
    SECTION_HEADING_H and BUTTON_PAIR_REL as positive numbers on the instance
    NS.Settings.Helpers IS. The second is the user-visible end of it: it renders a
    unit panel through Helpers.RenderUnitPanel and asserts every row spacer the
    render emits carries a positive numeric height — which is what the type check
    cannot cover on its own, because the forwarding binding is taken at file load.
    The source scan is kept unchanged as a supplementary lint.

    PRODUCTION MUTATION used to prove red: libs/LibKa0s/Options.lua:210
    `O.ROW_VSPACER = L.ROW_VSPACER` -> `O.ROW_VSPACER = nil`. Both new cases go red
    ("must be published as a positive number, got nil" and "row spacer 1 rendered
    with height nil"); the existing source-scan lint stays GREEN under it, which is
    the hole. Reverted: 754 passed, 0 failed.

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

commit f08ef1d7c2f71b8cb1d5a41ea26bd15a125bf484
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 15:44:23 2026 +0530

    docs(deviations): re-grade the two taint-§8 findings under the scoped rule  [M4-27]

    M1-STD-11 was resolved as OPTION (a): events-frames-taint-§8's pre-formatting
    MUST is now scoped to call sites whose arguments can reach a value read from
    one of the APIs it names, and a SHOULD everywhere else. So this item is a
    disposition, not a fix — no call site is converted and the cited code is
    untouched. What was needed was reading the live rule against this repo and
    recording the result.

    Findings graded against the live §8:

    - KCD-A-15 (`modules/Castbar_Debug.lua` pre-formatting) — PERMITTED, recorded as
      a note rather than a register row. KickCD uses none of the named APIs
      (verified: no UnitGetTotalAbsorbs / UnitHealth / UnitThreatSituation /
      C_UnitAuras / UNIT_AURA anywhere in core, modules, settings), and the one
      combat-protected value it does handle — `current.notInterruptible` from
      UnitCastingInfo/UnitChannelInfo, which "any other API a client build protects
      in combat" does put in the trigger set — is never formatted. Those lines build
      from its `type()` and from `issecretvalue()`'s boolean, and the
      `NINT_REPORT.boolean` arm is reachable only once the type already IS boolean.
      No KickCD pre-formatting site sits on a trigger-set value, so the MUST fires
      nowhere here.

    - KCD-A-04 (`core/Compat.lua:373-381`'s second value renderer) — does NOT
      dissolve under the scoping, which touches only the pre-formatting rule.
      §8's closing line still makes a hand-rolled stringifier the deviation, so it
      is recorded as a register row in docs/ARCHITECTURE.md with its rationale (a
      diagnostic value column, not the printer seam; `issecretvalue()` is what the
      concat probe approximates, not the forbidden `..` probe; every value reaching
      the format has already been sanitized) and a re-check trigger.

    Also reported, NOT fixed — out of this item's scope: `modules/Castbar_Debug.lua:125`
    binds `NS.Util and NS.Util.print or _G.print`. §8's ban on the global `print()`
    for user-facing output is one of the two things the scoping explicitly does not
    relax. The fallback arm is unreachable today (core/CoreSetup.lua defines
    Util.print on both the library-present and library-absent paths), so it grades
    Info — but it is dead code reading as a sanctioned escape hatch and is written
    down in the register section so it is not lost.

    Register housekeeping: the stray blank line that split the deviation table into
    two fragments is removed, so the "single home" the section claims is one table.

    Closes KCD-A-04, KCD-A-15.

    Verified: luacheck 0 warnings / 0 errors in 33 files; lua5.1 tests/run.lua
    752 passed / 0 failed; lizard Warning cnt 0; tests/perf.lua 0 failures. No Lua
    file changed.

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

commit 92511dbdd8807a5b209cc2f692d2f3e06a9672df
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 15:40:36 2026 +0530

    fix(debuglog): stop the degraded stub re-spelling the library's line and hexes  [M4-26]

    debug-logging-§3 forbids a fallback stub from reproducing the library's line
    format OR its color codes, and debug-logging-§7 still requires the stub to flip
    the flag and print the ack. KickCD's stub did both wrong ways round: its
    FormatPlain was byte-identical to libs/LibKa0s/DebugLog.lua's lib.FormatPlain,
    and SetEnabled printed the library's own green/red state hexes — while the
    file's own comment claimed "no formatter ... say nothing about color", arguing
    only the color half and missing the format entirely.

    - FormatPlain (and the FormatColored alias) now hand back the message verbatim.
      The stub has no console, so it has no line to render, and inventing a shape
      would be a second format to keep in step with the library's. Nothing in the
      addon calls them; they exist for the surface parity debug-logging-§7 asks for.
    - The ack still prints — "debug logging ON" / "OFF" — uncolored. The ack is the
      MUST; the state hexes are the library's string, not this file's to copy.
    - The :58-62 comment is rewritten to argue the right half, and deliberately does
      not quote the forbidden shape, so a grep for a hand-copied format cannot find
      one in the file that swore off it.

    Closes KCD-A-14.

    Tests: the ack case now also pins that the STATE WORD carries no color escape
    (the [KCD] tag's own color is NS.Util.print's and is out of scope); a new case
    pins that the stub renders no line; and the source-scanning case grew from two
    probes to five — it missed both of the copies that were actually there.
    Falsifiability: re-planting the old FormatPlain body and the old hex-bearing ack
    reddens three cases (749 passed / 3 failed) with "the stub must hand back the
    message, not a formatted line" and "must not carry the plain line's separator";
    reverting returns 752/752.

    Also repairs docs/test-cases.md: M4-02 regenerated it through a shared /tmp path
    that a concurrently-running sibling repo overwrote, so the committed inventory
    was another addon's. It is regenerated here from this repo's own `--list`, and
    `--list` now diffs clean against it CR-normalized.

    Verified: luacheck 0 warnings / 0 errors in 33 files; lua5.1 tests/run.lua
    752 passed / 0 failed; lizard Warning cnt 0;
    `grep -nE '\|cff40ff40|<ts> \| \[' core/DebugLogSetup.lua` returns nothing.

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

commit 789bb8595d4bf2faa1d2b14243e78e79f9e19b51
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 15:31:40 2026 +0530

    perf(offline): ship the scenario runner and stop citing an uncommitted capture  [M4-25]

    performance-§9 wants an offline scenario runner at tests/perf.lua with a
    zero-overhead scenario; performance-§8 wants docs/perf-runs/ with a README;
    documentation-§3 wants docs/performance.md. KickCD had none of the three, so
    its automated-test manifest recorded `perf: skip` — and core/PerfSetup.lua
    quoted "125.02 ms / 51.14 / 73.9 ms" from a capture that was never committed,
    which under performance-§8 is an assertion wearing evidence's clothes.

    - tests/perf.lua drives the addon's real chain under the headless mock, with
      both load lists DERIVED (LibKa0s.xml + the TOC) so a short list cannot
      silently measure the degradation stub. Five scenarios: spellPoll (asserts
      exactly 3 WoW API calls per watched spell), spellState, iconApply, and the
      probeOverheadOff/On pair performance-§9 requires by name. Assertions are on
      API counts and bytes/iteration only — never wall-clock.
    - The zero-overhead pair carries three assertions: an ABSOLUTE 900-byte ceiling
      on the dormant arm (measured 848.0 — the relation alone cannot catch a
      regression that lifts both arms together), dormant <= armed, and equal API
      counts.
    - docs/performance.md: the two harnesses, the eight buckets and their declared
      nesting, why `visibility` is not nested and `glowGate` not declared, the
      offline scenario table, the real `/kcd perf` verb set, and the suspend
      contract's two halves.
    - docs/perf-runs/README.md: naming, the schema-2 sketch (every figure zeroed,
      so nothing in it can be misread as a capture), the field notes, and a plain
      statement that the directory is EMPTY and that is the gap.
    - core/PerfSetup.lua: the uncommitted figures are removed and replaced with a
      note saying why. The two decisions they backed — instrumenting both PollSpell
      exits, and un-nesting `visibility` — keep their reasoning, restated so it
      stands on structure (RefreshVisibility has seven call sites, six of them not
      cast events) rather than on unquotable numbers.

    Closes KCD-R-07, KCD-A-05, KCD-A-06.

    Falsifiability: breaking the gating idiom in Icon:Apply (`local __t0 = ({
    Perf.on and debugprofilestop() })[1]`) lifts the dormant arm to 928.0 bytes and
    the runner exits 1 with "over the 900-byte ceiling"; reverting exits 0.

    Verified: luacheck 0/0 in 33 files; tests/run.lua 751 passed / 0 failed; lizard
    Warning cnt 0; `lua5.1 tests/perf.lua` 0 assertion failures; and
    `tests/_kit/run-automated-tests.sh --suite perf --no-bundle` now reports
    `perf  pass — 5 scenarios`, not skip.

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

commit ec20e81e940d8ebb4fcf9629e927c9f9d5d9dea5
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 15:20:47 2026 +0530

    refactor(defaults): give the profile tree one declaration site  [M4-02]

    savedvariables-§2 wants every profile default declared in defaults/Profile.lua
    and nowhere else. KickCD had it in core/Database.lua, and a SECOND hardcoded
    copy of the two per-state cast-bar tables in modules/Castbar.lua — which had
    drifted from the first in both directions that matter: it stored its colors
    positionally where the defaults tree stores them keyed (the v3→v4 color-shape
    migration's whole subject), and it disagreed about the interruptible border
    (borderShow=false/size 1 versus true/2). A profile that fell back therefore
    rendered differently from a fresh one. The duplicate is the finding.

    - New defaults/Profile.lua carries ICONS_DEFAULT, CASTBAR_DEFAULT,
      LABELSTYLE_DEFAULT and DEFAULT_PROFILE verbatim, published as NS.C /
      NS.DEFAULT_PROFILE plus NS.CASTBAR_DEFAULT / NS.LABELSTYLE_DEFAULT for the
      two callers that need a sub-tree by name. No content edit to the tree.
    - KickCD.toc gains defaults\Profile.lua ahead of defaults\Spells.lua. `#
      Defaults` loads after `# Core`, so core/Database.lua reaches the tree at CALL
      time: the AceDB defaults table is now built by aceDBDefaults() from InitDB,
      and BackfillLabelStyle reads NS.LABELSTYLE_DEFAULT inside the function.
    - modules/Castbar.lua's INT_FALLBACK / UNINT_FALLBACK are deleted and read off
      NS.CASTBAR_DEFAULT at load (modules load after defaults). Both consumers —
      Castbar:ApplyState and Castbar_Skin:Reskin — are read-only, so sharing the
      tables cannot write back into the defaults.
    - Docs repointed at the new home: ARCHITECTURE.md (owner table + load order),
      module-map.md (tree, load order, lifecycle), conventions.md,
      saved-variables.md, smoke-tests.md.

    Closes KCD-A-10.

    Test: the old case asserted the DIVERGED copy's values ("gold with no border"),
    so it is replaced by two: one pinning that each fallback IS defaults/Profile's
    table (rawequal — equality would still pass on a re-forked copy), one keeping
    the gold-vs-red distinguishability, read through UnpackColor so it does not
    re-encode a shape. Falsifiability: re-planting the old positional literal as
    INT_FALLBACK reddens "each state fallback IS the shipped default" (750 passed,
    1 failed); reverting returns 751/751.

    Verified: luacheck 0 warnings / 0 errors in 33 files; lua5.1 tests/run.lua
    751 passed / 0 failed; lizard Warning cnt 0; docs/test-cases.md regenerated and
    `--list` diffs clean CR-normalized.

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

commit 2621a43818e6bd1bf1a5efa36518265227276ad0
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 15:06:49 2026 +0530

    fix(slash): pcall the GateHint probe so the gate is restored unconditionally  [M4-03]

    settings/Slash.lua's NS.Slash.GateHint answers "why did that dropdown value get
    rejected?" by swapping the gating setting's stored value to each candidate,
    re-asking the row's own `values()`, and restoring. The comment above it records
    the swap as deliberate and transient — but the one call inside the window is
    addon-authored and free to raise (an LSM-backed row reads another addon's
    table). If it did, the restore never ran and the swapped-in candidate stayed in
    SavedVariables: a silent write produced by a READ-ONLY hint, outside the
    addon's single write seam, with no onChange and no panel refresh.

    The probe now runs under pcall, so the restore line is unconditional and a
    raising `values()` costs the user one missing "flip X to Y" clause instead of a
    changed setting. The docstring says so and points at the case.

    New case in tests/test_color_shape.lua: build a row carrying a real valueGate but
    a `values` that raises, call GateHint, and assert both that it does not
    propagate and that H.Get(row.valueGate) is byte-identical to its pre-call value.

    Closes KCD-R-06, KCD-A-17 (cluster C19+C24+C28 — one write seam; a carve-out
    that writes the live profile and restores it wraps the window in a pcall).

    Verification: luacheck . 0/0 in 32 files; lua5.1 tests/run.lua 750 passed, 0
    failed (749 -> 750, the one added case); lizard 0 warnings; docs/test-cases.md
    regenerated and `diff` against `tests/run.lua --list` is empty with both sides
    CR-normalized; git status shows only the three intended paths.
    Mutation: reverting the probe to `local alt = allowedKeys(row)` with no pcall
    turned the new case red — "GateHint must not propagate the row's error; got:
    values() raised mid-probe", 749 passed / 1 failed. Restoring the pcall returned
    it to 750 passed / 0 failed.

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

commit 9a172d4ed86bfcc1e11ee512a4d4382ad81159c0
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 15:01:30 2026 +0530

    refactor(bus): give CONFIG_CHANGED and PROFILE_CHANGED one sender each  [M4-04]

    architecture-§4 wants one emitter per message. KickCD had six SendMessage sites
    for Ka0s_KickCD_CONFIG_CHANGED (core/KickCD.lua's lock toggle and its slash
    spells commit, modules/IconGrid.lua and modules/Castbar.lua OnDragStop,
    settings/Spells.lua's throttled commit, settings/Panel.lua's Helpers.Set) and two
    for Ka0s_KickCD_PROFILE_CHANGED (core/Database.lua's OnProfileChanged and
    ResetAllSpells).

    settings/Panel.lua already had the named emitter, Helpers.FireConfigChanged, and
    six call sites in settings/Panel_Render.lua already used it — the five outliers
    were the ones that had rebuilt it inline. They now call it too. It lives on
    NS.Settings.Helpers, which settings/Panel.lua populates as host code whether or
    not LibKa0s is present, so the degraded path is unaffected.

    core/Database.lua gains a file-local fireProfileChanged(key); both of its paths
    call it.

    grep -rn 'SendMessage("Ka0s_KickCD_' core modules settings now returns exactly
    one line for each of the two messages.

    docs/ARCHITECTURE.md's bus catalog and docs/message-bus.md's table both named
    the old multi-sender lists; both now name the one emitter and list its callers.
    message-bus.md's "adding a message" rule said a second emitter merely needs a
    recorded justification — it now says to add a named emitter instead, and states
    honestly that SPELL_STATE (three sites) and GRID_LAYOUT (two) remain multi-site
    inside their single owning module, which is what the rule actually requires.

    Closes KCD-A-08 (cluster C17/architecture-§4 — the bus catalog matches the code).

    Verification: luacheck . 0/0 in 32 files; lua5.1 tests/run.lua 749 passed, 0
    failed; lizard 0 warnings; git status shows only the seven intended paths.
    Mutation: deleting the one SendMessage line inside Helpers.FireConfigChanged
    turned tests/run.lua red (748 passed, 1 failed) — the routed paths really do
    depend on that single seam. Reverted; 749/0 again. A throwaway probe suite (added
    and removed, not committed) also confirmed NS.Settings.Helpers.FireConfigChanged
    is a live function on a fully-enabled instance and dispatches to a bus target.

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

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

    docs(i18n): sweep the British spellings out of the CCN-elimination plan  [M4-28]

    docs/superpowers/plans/2026-08-04-ccn-elimination.md is KickCD's own plan file,
    not a frozen audit or review bundle, so localization-§5's US-English rule binds
    it. Nine lines carried "colour"/"colours"/"Colour"/"Colours" — :51, :53, :57,
    :117, :121, :123, :147, :149, :213, exactly the set the finding names (the audit
    bundle had listed only five of them). All nine are now the US form; no other
    British term appears in the file.

    Closes KCD-A-16 (cluster C27 — US English is fixed at the source).

    Verification, all with the GFM backslashes stripped from the pattern:
    - BEFORE: grep -cniE "colour|behaviour|grey|synthesise|normalis|recognis" <file>
      -> 9, on those nine lines.
    - AFTER: -> 0.
    - MUTATION: planted "(this documents the behaviour, in grey)" into :121 -> the
      same grep went to 1 and named the planted line, proving the check fires on the
      terms beyond "colour" too. Reverted -> back to 0.
    luacheck . 0/0 in 32 files; lua5.1 tests/run.lua 749 passed, 0 failed; lizard 0
    warnings; git status shows only the plan file.

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

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

    docs(readme): drop the angle-bracket placeholder from the Troubleshooting row  [M4-08]

    README.md:191 rendered `/kcd reset <setting>`; the Slash-commands table at :75
    already uses the bare `/kcd reset setting` form, so the one row that kept the
    angle brackets was the only place a reader was shown a syntax the rest of the
    document does not use.

    Closes KCD-A-07 (cluster C5 — the root doc set and the README structure are
    canonical).

    Verification: `grep -n '<setting>' README.md` empty; luacheck . 0/0 in 32 files;
    lua5.1 tests/run.lua 749 passed, 0 failed; lizard 0 warnings; git status shows
    only README.md.

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

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

    docs(comments): correct the two comments whose count and citation do not resolve  [M4-10]

    core/PerfSetup.lua's bracket-declaration note claimed "All four of PollSpell's
    exits are instrumented now". PollSpell has TWO exits — the `return nil` guard
    and the `return state` tail — which is what modules/Cooldowns.lua's own header
    over the function has always said. The count now agrees with the code.

    settings/Slash.lua and tests/test_coresetup.lua both cited the sibling
    AbsorbTracker stub as "(settings/Slash.lua:383)". Read from this repo the path
    names THIS file, which is 347 lines, so the citation resolved to nothing; and
    AbsorbTracker's own line 383 is inside its profile dispatcher, not the stub. The
    citation now names the sibling path (../AbsorbTracker/settings/Slash.lua) and the
    symbol (`missing`), both of which resolve, and drops the volatile line number.

    Closes KCD-R-09, KCD-R-10 (cluster C3 — a comment's named file or caller exists).

    Verification: `ls ../AbsorbTracker/settings/Slash.lua` resolves and `missing` has
    one definition there; luacheck . 0/0 in 32 files; lua5.1 tests/run.lua 749
    passed, 0 failed; lizard 0 warnings; git status shows only the three paths.

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

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

    refactor(exports): drop the unread NS.Castbar export and the dead lint allowlist entry  [M4-07]

    modules/Castbar.lua published `NS.Castbar = Castbar` with no reader anywhere in
    core/, modules/, settings/ or tests/ — the module is reached through Ace3's
    `self:GetModule("Castbar")` (core/KickCD.lua:210) and never off the namespace.
    Removed.

    .luacheckrc read_globals allowlisted `InterfaceOptionsFrame_OpenToCategory`, an
    API removed in 10.0. The addon opens its panel with `Settings.OpenToCategory`
    (core/KickCD.lua), so the entry allowlisted a symbol no linted file mentions.
    Removed.

    Closes KCD-R-11, KCD-R-12 (cluster C7 — an exported symbol has a caller or a
    documented reason).

    Verification: luacheck . 0 warnings / 0 errors in 32 files; lua5.1 tests/run.lua
    749 passed, 0 failed; lizard 0 warnings; git status shows only the two paths.

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

commit 136b3740ad0a8ae368d1df0ef39a657d6a738787
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 14:38:22 2026 +0530

    refactor(options): read every layout constant and shared member off the instance  [M3-13]

    options-ui-§8 and options-ui-§1: a host MUST NOT restate the options library's
    layout constants or keep its own copy of a published member. KickCD did both,
    and settings/Panel.lua stated the rule in a comment and broke it twenty lines
    below the comment.

    CONSTANTS (KCD-A-03)
    - core/Constants.lua: `Const.PANEL_PADDING_X = 16` deleted, restating
      lib.LAYOUT.PADDING_X. The file now carries a comment saying why it is absent.
    - settings/Panel.lua: `local ROW_VSPACER = 8` deleted along with the
      `Helpers.ROW_VSPACER = ROW_VSPACER` line that ASSIGNED IT OVER the library's
      published O.ROW_VSPACER — so the library's value could not have won even
      where a caller correctly read the instance.
    - tests/test_constants.lua drops PANEL_PADDING_X from the positive-number list.
    - docs/module-map.md drops it from core/Constants.lua's constant list.

    MEMBERS (KCD-A-02) — three of the four cited host copies dropped, each verified
    equivalent to the library member it shadowed before deletion:
    - attachTooltip / Helpers.AttachTooltip  -> O.AttachTooltip  (OptionsWidgets)
    - ensureScroll  / Helpers.EnsureScroll   -> O.EnsureScroll   (Options)
    - addSpacer     / Helpers.AddSpacer      -> O.AddSpacer      (OptionsWidgets)
    - addBlock, a fifth copy of the same body with a return the three call sites
      ignore, also dropped; O.AddSpacer already returns the widget.
    Pixel-neutral: the library's EnsureScroll applies the identical inset from
    lib.LAYOUT.PADDING_X (16), and O.ROW_VSPACER is the same 8.

    NEW CASE, tests/test_constants.lua: "no host copy of a LibKa0s-Options layout
    constant" — source-scans core/Constants.lua and the three settings/Panel*
    files for a numeric-literal assignment to any of the four published names.
    Source-scanned per testing-§11 because a reintroduced copy is a DECLARATION:
    while the two copies agree, no runtime observation can tell "read the
    library's 8" from "read a host 8", which is the entire failure mode.

    RED/GREEN: restored both host copies -> FAIL naming
    core/Constants.lua:158 and settings/Panel.lua:358 (expected 0, got 2),
    748 passed / 1 failed; reverted -> 749 passed / 0 failed. Confirmed the
    compliant read shape (`local ROW_VSPACER = Helpers.ROW_VSPACER`,
    settings/Panel_Render.lua:20) does not trip it.

    SURFACE PARITY. M3-04's gate went red the moment the host copy was deleted:
    ROW_VSPACER was present on the degraded path only because Panel.lua published
    it unconditionally — parity held for the wrong reason, with the host copy
    filling the degraded hole. Adding it to the stub is forbidden (options-ui-§1;
    tests/test_options_panel.lua:425 already fails any `ROW_VSPACER =` there), so
    it joins PADDING_X / SECTION_HEADING_H / BUTTON_PAIR_REL in
    tests/test_surface_parity.lua's live-only exclusion list, with the reason
    recorded. On the degraded path it is nil, bound into a local that is forwarded
    to a no-op AddSpacer.

    TWO DEVIATIONS FROM THE PLAN'S LETTER, both because the plan is wrong about a
    measured fact:

    1. M3-13 and 03_SPEC.md C21 both require "settings/Panel.lua:307 reads
       O.PADDING_X". It cannot: PADDING_X's ONLY consumer in this file was the
       host `ensureScroll` copy that the SAME work item deletes (lines 363-364,
       the scroll insets). Keeping a read with no consumer is an unused local and
       luacheck fails it. Deleting Const.PANEL_PADDING_X outright is strictly
       stronger than the acceptance asked for — no host copy AND no host read —
       and the guard case above pins it.

    2. The fourth cited host copy, `Helpers.LSMValues` (settings/Panel.lua:281,
       the one line number that had not drifted), is NOT dropped, and the plan's
       own adversarial note already flags this finding's sharpest sub-claim as
       wrong. Three measured blockers:
       - O.LSMValues returns a DEFERRED CLOSURE; the host's returns a TABLE. The
         eight call sites wrap in a closure, so substituting would hand the widget
         a function that returns a function.
       - The library falls back to STRINGS.LSM_NONE ("None"); the host uses
         "Default". That is a user-visible change, and M3-13's own acceptance is
         that nothing moves.
       - Load order: settings/Icons.lua and settings/Castbar.lua evaluate
         H.LSMValues(...) inside schema-row literals AT FILE LOAD. As an instance
         member the LibKa0s-absent stub must publish it or those page files raise,
         the rows never register, and NS.Settings.Schema loses a large part of
         itself — silently taking /kcd list|get|set|reset and the profile defaults
         with it. settings/OptionsSetup.lua:140-167 records this as measured, and
         tests/test_options_panel.lua pins it.
       Recorded instead as a third `## Documented deviations` row against
       options-ui-§1, with a re-check trigger that names the condition ending it:
       any KickCD page file stops evaluating these at file load.

    Closes KCD-A-03 and the three tractable quarters of KCD-A-02; the fourth is
    ratified rather than left open.

    Verified: luacheck 0/0; tests 749 passed 0 failed (was 748, +1 as designed);
    lizard Warning cnt 0; docs/test-cases.md regenerated and diffs clean
    CR-normalized; §N.M five-path sweep still 0.

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

commit 6862565c3a4fb1ab37e91f06f66ba777868aa8ed
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 14:23:47 2026 +0530

    fix(perf): close the two leaked bracket exits, and pin that none can leak again  [M3-12]

    Two Perf brackets opened and returned without closing, so their buckets
    reported a `calls` count short by exactly the number of times the leaked exit
    was taken — which reads as a cheap path, not as a bug:

    - modules/IconGrid_Render.lua `_tickAllTextIcons` — the empty-set guard
      returned unclosed. That exit is taken on the LAST tick of every cooldown
      burst, so `cdText` under-counted by one call per burst and dropped the
      ticker-teardown cost entirely.
    - modules/Castbar.lua `onUpdate` — the no-duration guard returned unclosed.
      That exit is taken once per cast, on the frame that tears the OnUpdate
      handler down, so `castTick` under-counted by one call per cast.

    Both now close on the shape modules/Cooldowns.lua:190-202 already used
    (`if __t0 then Perf.Note(<bucket>, debugprofilestop() - __t0) end` before the
    `return`), which is where that comment's claim that "tests/test_perfsetup.lua
    pins that no exit is left unclosed" came from — it pinned PollSpell only.

    NEW CASE, tests/test_perfsetup.lua: "no bracketed function leaks an exit —
    every return closes the bracket". It walks every top-level function in the
    four bracketed modules, and for any that opens `__t0` requires that every
    `return` be preceded by a Perf.Note and that the function close its bracket
    at least once.

    These are Note-shaped, not Open/Close-shaped, so LibKa0s CANNOT detect them
    — there is no stack to leave unbalanced (03_SPEC.md C9 says so explicitly).
    Behavioral coverage is unavailable too: `cdText` is driven by a C_Timer
    ticker and `castTick` by an OnUpdate script, neither of which the harness
    drives — which is precisely why these two survived a suite that already had a
    hand-written four-exit case for PollSpell. testing-§11's source-scan shape is
    the correct fallback and the case comment carries its justification.

    RED/GREEN, both arms of the new case:
      plant 1 — revert both fixes (the exact pre-M3-12 state):
        FAIL ... IconGrid_Render.lua:826 _tickAllTextIcons — unclosed `return`
                 Castbar.lua:689 onUpdate — unclosed `return`   (expected 0, got 2)
        747 passed, 1 failed
      revert -> 748 passed, 0 failed
      plant 2 — delete every cdText Note so the bracket is never closed:
        FAIL ... unclosed `return` AND "opens a bracket it never closes"
        746 passed, 2 failed
      revert -> 748 passed, 0 failed

    Closes KCD-R-05, KCD-A-18.

    Verified: luacheck 0/0; tests 748 passed 0 failed (was 747, +1 as designed);
    lizard Warning cnt 0; docs/test-cases.md regenerated and diffs clean against
    `lua5.1 tests/run.lua --list` with both sides CR-normalized.

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

commit 2742a95fa0e56d5db18411f3859c261dc9394f02
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 14:16:18 2026 +0530

    docs(deviations): give KickCD's register the mandated shape and one home  [M3-08]

    documentation-§3 makes `## Documented deviations` the ninth mandated section
    of docs/ARCHITECTURE.md, with the exact row shape
    `| Rule | What differs | Why | Decided | Re-check trigger |`, and makes it
    the SINGLE home for a ratified deviation. KickCD's register was six
    "Deviation recorded as intentional" paragraphs scattered across four topic
    docs in an ad-hoc shape, with no rule citation, no decision date and no
    re-check trigger — which is exactly the state that gets a ratified decision
    re-filed as an open MUST failure every audit cycle.

    Added the section after `## Known limitations` with two rows, both against
    `savedvariables-§1`:

    - the two profile migrators that run off the stored SHAPE rather than off
      db.global.schemaVersion (reasoned at docs/saved-variables.md:212)
    - the DEFAULT_PROFILE restructure that nested target/focus under
      units.<unit> (reasoned at docs/saved-variables.md:222)

    Decided dates are the dates the decisions were actually recorded, recovered
    with `git log -S` (2026-07-16 and 2026-07-15) rather than backdated to today.
    Both re-check triggers name a condition that ENDS the deviation and can be
    observed to have fired.

    The two source paragraphs now say the register is the record and they are the
    reasoning, per the section's "MAY be reasoned at length elsewhere" clause.

    FOUR OTHER RECORDS DELIBERATELY NOT ROWED, each named in the section with the
    reason, so a later audit does not read the omission as an oversight:

    - the two KickCD<Widget><UnitTitleCase> frame-naming notes
      (docs/conventions.md:19-20) and the additive GRID_LAYOUT payload note
      (docs/message-bus.md:49) depart from THIS ADDON'S conventions, not from a
      numbered rule; naming-cheatsheet has no subsections, so there is no
      filename-§N to cite and a register of standards deviations is the wrong
      home for them.
    - the Ka0s_KickCD_PROFILE_CHANGED two-emitter note
      (docs/ARCHITECTURE.md:79) IS a real architecture-§4 deviation, but it is
      being FIXED, not ratified: M4-04 reduces both CONFIG_CHANGED and
      PROFILE_CHANGED to one sender. Rowing it here would have created a
      register entry that M4-04 then has to retire in the same milestone.

    Verified: grep -n "Documented deviations" docs/ARCHITECTURE.md resolves;
    both rows match ^| `<filename>-§N` | … | YYYY-MM-DD | …; no §N.M
    reintroduced. luacheck 0/0; tests 747 passed 0 failed; lizard Warning cnt 0.

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

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

    docs(refs): retire the global §N.M notation for filename-§N  [M3-07]

    Comment-only. Every retired global `§N.M` reference in KickCD's code, tests,
    docs and config is rewritten to the `filename-§N` scheme
    (documentation-§6). 36 sites across 22 files, plus the regenerated
    docs/test-cases.md, whose case titles carry six of them.

    The mapping was derived from the pre-split standard
    (WowAddonStandards 070caf2^:standards/01_STANDARD.md) and then RANGE-CHECKED
    against the current section files, not assumed:

      §1.2  Tier 2 — Modular (1500 LOC cap)  -> layout-§1
      §2.2  SavedVariables naming            -> toc-file-§2
      §3.4  Lib registry pattern             -> library-stack-§4
      §4.1  Namespace bootstrap              -> architecture-§1
      §4.4  Closed message bus               -> architecture-§4
      §5.1  AceDB structure                  -> savedvariables-§1
      §7.4  Help output & chat tag           -> slash-commands-§4
      §12.2 Monospace font (shipped)         -> debug-logging-§2
      §12.3 Line format                      -> debug-logging-§3
      §12.4 The sink                         -> debug-logging-§4
      §12.5 Enabled-state, session-only      -> debug-logging-§5

    Only layout renumbered in the v1.5.0 split (the three Tier-1/Tier-2/Casing
    subsections collapsed to one "Modular layout"); every other subsection kept
    its ordinal, which is why the mapping looks like an identity and had to be
    checked rather than trusted.

    Swept with ALL FIVE exclusions — libs/, tests/_kit/, docs/audits/,
    docs/reviews/ AND docs/automated-tests/. KickCD is the repo where the fifth
    one decides the outcome.

    Closes the KickCD row of the C15 cluster (AT-A-11, BL-A-11, CM-A-20,
    LH-A-19, WG-A-15 are the sibling repos' ids).

    Verified:
      five-path exclusion  -> 0 hits (was 41)
      four-path exclusion  -> 30 hits, ALL of them inside the three frozen
        automated-test bundles 20260804-182144/, -214315/, -233245/
        (test-cases.md + tests.txt), exactly the 30 the plan predicted would
        and must survive as evidence.
      Every remaining filename-§N reference in the repo range-checks against
        its section file's local heading count.
      docs/test-cases.md regenerated; `lua5.1 tests/run.lua --list` diffs clean
        against it with both sides CR-normalized.
      luacheck 0/0; tests 747 passed 0 failed; lizard Warning cnt 0.

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

commit 1117274b218c94645dfdf10c0d4f4b6637d7e70f
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 14:06:32 2026 +0530

    docs(gates): name the checkpoint in every gate statement  [M3-06]

    Sweep 3f. A bare "Gates? yes/no" column cannot be written honestly: there
    are two checkpoints — the run/commit and the tag — and perf and complexity
    answer differently at each. Every hand-written gate statement now names both.

    - docs/testing.md: the Gates? table splits into "Gates the run and the
      commit?" and "Gates the tag?"; the paragraph below it states that perf
      and complexity never fail a run and never block a commit, but DO gate the
      tag (all four at pass plus zero functions above CCN 15, evaluated by
      /wow-addon:bump-version from the release run's manifest.json); the "At
      release, not at commit" paragraph says what the tag is gated on.
    - docs/automated-tests/README.md: same split under the section heading,
      renamed "What gates at which checkpoint"; the skip sentence now says a
      skip is NOT EVALUATED at the release gate rather than passed.
    - CLAUDE.md: "Complexity — recorded, never a gate" becomes "recorded at the
      commit, gating at the tag".
    - docs/automated-tests/RESULTS.md: the lead-in is replaced with the text the
      re-vendored v1.8.0 runner emits verbatim. Every past row is untouched.

    PLAN CORRECTION. M3-06 says "The RESULTS.md lead-in comes from the
    re-vendored runner, not from this sweep." It does not. The runner writes
    that block only in the branch that CREATES RESULTS.md
    (tests/_kit/run-automated-tests.sh:414-433); when the file exists and its
    header matches, it inserts the row and nothing else (:403-406). No runner
    invocation would ever have replaced the stale lead-in in this repo, so the
    acceptance grep was unreachable without a hand edit. The replacement text is
    copied byte-for-byte from the runner's own printf block, so the next fresh
    bundle in a new repo produces the identical wording.

    Closes KCD-A-13 (the C8 cluster's KickCD row).

    Verified: grep -rn "never fail a run" docs/ CLAUDE.md returns only
    checkpoint-qualified sentences plus the frozen docs/audits/ evidence line,
    which is untouched by design. luacheck 0/0; tests 747 passed 0 failed;
    lizard Warning cnt 0.

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

This mod has no additional files