promotional bannermobile promotional banner

Ka0s WhatGroup

Notifies you of group details after joining via Premade Group Finder
Back to Files

1.4.0-release

File nameWhatGroup-1.4.0-release.zip
Uploaded
Sep 10, 2026
Downloads
29
Size
6.2 MB
Flavors
Retail
File ID
8853256
Type
R
Release
Supported game versions
  • 12.1.0

What's new

commit 7ff1823d00daea1d3b85e6b179ffb3266b2e7080
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Sep 11 00:55:31 2026 +0530

    sync-docs: four citations that had drifted off their lines

    DEPENDENCIES.md's evidence column pointed at docs/testing.md by line, and four of
    those ranges had slipped: the diff -r block is 218-221 not 217-220, the
    renormalize hint is 261 not 259, the vendored runner is 338-340 not 336-338, and
    the no-CI statement is 174-176 not 173-175. Each was off by enough to miss a
    command or fall on a fence.

    ARCHITECTURE.md said the legacy GetSpell* globals are the fallback rung for each
    of four C_Spell APIs. GetSpellLink has no rung -- core/Compat.lua:52-57 degrades
    straight to nil -- which is what docs/compat-layer.md:15 already recorded.

    WhatGroup.toc cited core/WhatGroup.lua:121 for the NS.FONT_MONO assignment that
    makes the following load order load-bearing; it is :128. .luacheckrc said a
    top-level ignore would reach all 40 files; luacheck reads 41.

    Target interface to 120100 in DEPENDENCIES.md, ARCHITECTURE.md, scope.md,
    module-map.md and common-tasks.md's worked example.

    docs/perf-analysis/README.md stays absent and that is correct: ARCHITECTURE.md's
    ratified performance-§12 row names 'no docs/perf-analysis/' among the declined
    wiring, and the Documentation map files the doc Not applicable with its trigger.

    docs/test-cases.md regenerated. luacheck clean at 41 files, 568 tests green.
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01G4Xy2KrRa8NmD1843rgKnE

commit bed07dd61ae6d093978db4982257b45a8d44140d
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Sep 10 23:55:14 2026 +0530

    Release 1.4.0

    Show and Close are combat-safe now — the popup is no longer asked to hide while
    protected — a popup you closed stays closed, the visibility gate is re-asked
    when combat starts and ends, and each capture is paired to its own application
    rather than to whichever answered first. 136 commits since the 1.3.0 tag.

    Release gate passed at 53da443: luacheck 0/0 across 41 files, 568 tests green,
    zero functions above CCN 15. The perf suite did NOT run — no tests/perf.lua
    ships here — so the gate covered three suites, not four, as the Version History
    row and ANALYSIS.md both state.

    Proportionally the largest test growth of the nine, +14 cases, which is the
    combat-safety work pinning itself down. The version fixtures in tests/ move
    with the TOC so the two cannot drift.

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

commit 53da4438d59a34e0a534483e834f4c78eb99b337
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Sep 10 23:45:02 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 fab575c0b96e42535f150ee56346bd8292303287
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Sep 10 23:26:23 2026 +0530

    README: say it is the Group Finder, drop the settings shot

    "Apply through the Premade Group Finder, join, shut the LFG window" made a
    reader assemble the trigger themselves. It now says what it is: the group
    you signed up for through the Group Finder, once you are accepted. The logo
    block and the settings-panel screenshot go, and the popup and chat-link
    shots move to the new attachment IDs with sentence-case captions.

    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 8298d324854d7d797fa4710a24a4f3a5bcbdf8a3
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Sep 10 03:31:57 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 8257927846b25957756e165c599f779cbcf84994
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Sep 10 02:51:51 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 8d99008b3abb1b1ecb6d870717a683e456dfac75
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Sep 9 21:43:25 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 e581517382b3152b4632f77e874d04fb031cfe3d
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Sep 9 01:41:55 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.
    568 passed, 0 failed, 0 skipped, 568 total
    Total: 0 warnings / 0 errors in 41 files

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

commit 4bccc0e132fc78a3c8e3501249de34b053a36fc0
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Sep 9 01:28:04 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.
    568 passed, 0 failed, 0 skipped, 568 total

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

commit 6ba6d83da72c4db2e0f9d57d2367248fac0a8b0c
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Sep 9 00:08:22 2026 +0530

    M2-31: Show is protected in combat too, so stop calling it there

    Reported from the client: `/wg test` mid-fight raised ADDON_ACTION_BLOCKED on
    WhatGroupFrame:Show(). The popup appeared anyway, which is worse than if it had
    not -- the error was the only signal that anything was wrong.

    This file asserted the opposite in as many words: "f:Show() on an already-built
    frame is not a secure write -- the secure work is buildFrame's, and it has
    already happened by the time f exists." Wrong for exactly the reason Hide is
    protected. The rule is about changing a PROTECTED FRAME's visibility, and
    showing an ancestor changes it as surely as hiding one does. M2-28 found the
    Hide half, wrote that sentence about Show in the same commit, and did not test
    it.

    The mock made the same mistake and is the reason this shipped: it modelled Hide
    protection and left Show alone, so `once built, showing during combat is
    allowed` passed for a year while the client refused the call. Show now walks the
    same subtree, and turning it on lit up exactly three cases -- all three
    asserting the bug.

    ShowFrame refuses in combat and defers to PLAYER_REGEN_ENABLED with the line the
    locale already carries. The one exception needs no protected call at all: a
    popup still SHOWN at alpha 0 comes back on its alpha alone, which is what keeps
    "close it during a fight" reversible instead of one-way. That is the owner's
    second requirement, and it is the mirror of M2-30's close.

    The finding this leaves open, stated rather than buried: `inCombat` asks for a
    popup that appears DURING a lockdown, which is the one thing Show cannot do. It
    now builds, stays off screen, and does not open -- silently correct instead of
    loudly broken. Delivering it means keeping the frame shown at alpha 0 for the
    whole time the player is OUT of combat so the edge needs only an alpha change,
    at the cost of an invisible 420x260 click-target at rest. That is a trade for
    the owner, not for this commit, and both docs/frame.md and smoke 4.5 step 9 say
    so.

    Four cases red first: a show requested in combat deferring rather than firing,
    the alpha-0 popup returning without a Show, and the two `inCombat` cases
    rewritten from what they wished were true to what the client permits.

    568 passed / 0 failed (was 567), luacheck 0/0 in 41 files. Smoke 4.5 gains steps
    7-9 and is NOT RUN; no client here.

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

commit 6726383e2725d9598383f44ea6c4348e392bdc8c
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 23:47:34 2026 +0530

    M2-30: Close works in combat, and 'out of combat' clears on the pull

    The owner's ruling, 2026-09-08: it must be possible to close an open popup in
    combat. Not opening one in combat is fine, provided it opens by itself
    afterwards.

    M2-28 concluded the opposite -- that the popup simply had to stay up for the
    fight -- and that conclusion was true of Hide and false of the requirement. The
    client refuses Hide on an ancestor of a SecureActionButtonTemplate button, so
    Hide was never the tool. Alpha is, and this file had already ruled so two
    functions above: ApplyFrameAlpha is deliberately un-guarded because opacity
    moves nothing, where ApplyFrameSize and ApplyFrameScale both refuse because they
    move the secure child. The answer was on the page the whole time and M2-28 did
    not go looking for it.

    So hidePopup takes the frame to alpha 0 in combat and owes the real Hide to the
    next legal edge, which ApplyFrameVisibility settles before it asks the gate
    anything. Close now works in both states, and `outOfCombat` clears on the pull
    rather than a fight later and comes back on its own when combat drops.

    What alpha does not buy is the frame leaving hit-testing: it is invisible, not
    absent, so until the lockdown lifts the title bar still drags and the teleport
    button still occupies its 24px -- a teleport cannot be cast in combat, so a
    click there does nothing. That residue is why pendingHide exists and why the
    soft state is never a resting one. Smoke 4.5 records it as known and accepted
    rather than leaving the next reader to find it.

    dismissPending is gone, subsumed: pendingHide carries the owed Hide and
    gateWithheld carries who owns the dismissal, and ApplyFrameVisibility saves
    gateWithheld across settling the debt because the real Hide fires OnHide, which
    clears it. Without that, a gate hide would come back as a player dismissal and
    never reopen -- the exact bug M2-29 fixed, re-entering through the back door.

    Five cases: Close in combat taking it off screen at once, the owed Hide landing
    with the alpha restored, the alpha restored being the player's own rather than a
    literal 1, `outOfCombat` clearing on the pull, and it reopening by itself
    afterwards. All five watched red against the previous implementation. Two older
    cases asserted IsShown alone, which now passes with the popup fully drawn; both
    tightened so they cannot pass vacuously.

    567 passed / 0 failed (was 562), luacheck 0/0 in 41 files. Smoke 4.5 rewritten
    to the new contract and NOT RUN -- no client here. Step 6 and step 7 are the
    ones that matter most: they prove the fix for closing did not take the
    legitimate re-show with it.

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

commit 15dd590d5793b8396b0d4e6ce07a847b7625e343
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 23:34:42 2026 +0530

    M2-29: a popup you closed stays closed

    Reported from the client: `/wg test`, close the popup, pull something, and it
    springs open again. No Lua error, because nothing about the call is wrong.

    M2-21 added the re-show half of the visibility gate as
    `if pendingInfo and not f:IsShown() then f:Show() end`. That treats "hidden" as
    one state when it is four, and only two of them are the gate's doing:

      never shown                        -- leave it shut
      the gate DECLINED a requested show -- reopen when the gate opens
      the gate HID a shown popup         -- reopen when the gate opens
      the player dismissed it            -- leave it shut

    The sharpest way to put it: under `always`, the SHIPPED DEFAULT, the gate never
    hides at all. So on the default value every firing of the re-show arm was a
    popup the player had closed. It had no legitimate case whatsoever, and that is
    how it reached a client through 559 green cases.

    M2-28 half-hid this. It added dismissPending for a Close pressed IN combat and
    refused by the client, so that path was already covered -- but an ordinary
    out-of-combat Close succeeds, returns true, and leaves no trace at all. The
    common case was the uncovered one.

    gateWithheld now carries why the popup is off screen, and the bookkeeping is
    inverted so the next hide path cannot forget it: OnHide clears it for EVERY
    hide, and only the gate's own two sites -- ShowFrame's refusal and
    ApplyFrameVisibility's hide -- set it back, immediately after. ESC goes through
    UISpecialFrames to a bare f:Hide() and leaves no other trace, so anything keyed
    off the Close button's handler would have been wrong by construction; this is
    keyed off the frame.

    Three cases, all watched red first: a Close-dismissed popup surviving a pull, an
    ESC-dismissed one doing the same, and a dismissal outliving the whole fight
    across both edges. The existing case that proves the LEGITIMATE re-show -- the
    gate withholding under `inCombat` and releasing on the pull -- stays green, and
    smoke step 4.6 step 5 is the in-client version of it, because a fix that killed
    the real case along with the bug would look identical from the outside.

    562 passed / 0 failed (was 559), luacheck 0/0 in 41 files. Step 4.6 is NOT RUN;
    no client is available here.

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

commit 0a4809d88dd21f3d6f54d02adc69e21bf55f1cf5
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 22:18:40 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 f56dcd3936c0578a448954e6858c44d542f8f899
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 21:51:21 2026 +0530

    M5-06: the amended rules, measured against the repository they govern

    A fresh read-only standards audit against v2.39.0 — the pass that checks the
    fifteen Group B amendments describe reality, rather than that a keyword was
    typed. Bundle only; no addon code, TOC or config is touched, and
    docs/audits/2026-09-07/ is untouched.

    14 roots / 15 total on 2026-09-07 → 7 roots / 8 total today. 0 High, 0 Medium,
    5 Low, 2 Info; 5 MUST failures, every one graded Low because none is reachable
    by a user, their SavedVariables or their session.

    Eleven roots and the one dependent closed, measured rather than assumed: the
    line-ending tree is clean at 0 strays (was 6), all four load-bearing TOC
    positions are annotated, the record is runner-generated with a passed/skipped/
    total cell, the register's §33 citation is a bare filename, docs/compat-layer.md
    exists over v2.39.0's new three-shim threshold, the README no longer names a tab
    that does not exist, and the fourth doc-map table this repo already shipped is
    now the rule.

    Three did not close and two are new — and both new ones were written by this
    cycle, hours after the sweep that closed their predecessors:

      WG-54  travelled and behaviour, both landed by M4c-04 into core/WhatGroup.lua
             after M4-13 had swept the file. Only localization-§5's newly published
             BRITISH list catches the first; the old five-word grep could not.
      WG-62  ten citations that resolve to nothing — lint-§1 at eight sites, written
             by M4-11 and M4c-04 while adopting the lint amendment, against a
             section documentation-§6 names as bare-filename-only; and
             code-quality-§3 at two, a section file that has never existed.
      WG-61  two register rows cite evidence ids that do not resolve — WG-R-06
             lands on an unrelated 2026-09-07 review finding, WG-A-08 on nothing at
             all. Visible only against audit-review-history's third MUST, new in
             v2.39.0. Its dependent WG-63 is the repo's own gate, green against both
             for two separate reasons its own header comment predicted.
      WG-51  the vendored gate still asserts no 100755. M1-LK-07 closed its sibling
             and not this half; LibKa0s HEAD has no assertion either.
      WG-58  .pkgmeta still does not ignore .superpowers. The .pkgmeta half closed
             by rule change; this half did not.

    Every count in the bundle is a recorded command with its scope stated, and
    every file:line was re-read and quoted beside its citation.

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

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

    M5-08: § 12b, and the login § 7a has been waiting five milestones for

    This addon had no non-English-client step, and almost everything it puts on
    screen about a group comes out of the client in the player's language: the
    activity's fullName and shortName from C_LFGList, the server-rendered
    playstyleString, the four GROUP_FINDER_GENERAL_PLAYSTYLE globals -- read once
    at file load, so a nil there is nil for the session -- and the spell name that
    goes straight into the teleport button's /cast macrotext. Nothing here has
    ever seen a German one: tests/wow_mock.lua answers enUS throughout, and the
    /wg test fixture spells the activity name out in English, which § 12b says
    plainly so nobody files the fixture as a bug.

    § 12b is written from that enumeration, and each step names its failure: an
    Unknown in the Instance row where the client plainly has a name, a name that
    overruns the popup's border (German activity names are the longest the client
    produces, and nothing else would notice), a playstyle global that came back
    nil, a teleport button drawn ready whose click casts nothing.

    Step 5 is the point of scheduling this at all. Session 6 owns § 7a as well as
    the locale pass -- one login, both jobs -- and § 7a is the
    C_SpellBook.IsSpellKnown observation WHATGROUP-R-06 has been gated on since it
    was filed. M5-10 correctly refused to ship the rung without it. So § 12b
    carries a fill-in block for the six readings the finding needs, including the
    client build and GetLocale(), and states both outcomes explicitly: agreement
    means M5-10 can add the rung in the shape the five siblings use, disagreement
    means the rung is NOT added and the disagreement is recorded on issue #15.
    Both close session 6's obligation. A blank is the only result that does not.
    The checklist and § 13's standing note about 7a both now point at it.

    Nothing here has been performed. No WoW client was available to this
    execution, so § 12b is marked NOT YET RUN, no step in this repository is
    recorded as passed, and WHATGROUP-R-06 stays exactly where M5-10 left it.

    The gate lands with the section: a case in tests/test_doc_structure.lua
    asserting the document still carries a non-English-client section, that it
    names a client to run it on, that it says what failing looks like, that it is
    more than a heading with a sentence under it, and -- the one assertion
    peculiar to this repo -- that it names C_SpellBook.IsSpellKnown, so the single
    login this addon needs cannot be spent without § 7a. It was watched failing on
    the "has no heading naming a non-English client" message before the section
    was written, and it claims nothing about the section having run.

    luacheck . is 0 warnings / 0 errors in 41 files; lua tests/run.lua is 559
    passed, 0 failed, 0 skipped, 559 total. The count moved by the one new case,
    so docs/test-cases.md is regenerated and the README badge moves with it here.
    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 13ae17ae0820e2bc10d1d9ca1861e65ac0e9e6a1
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 19:36:33 2026 +0530

    M5-08: § 12b, and the login § 7a has been waiting five milestones for

    This addon had no non-English-client step, and almost everything it puts on
    screen about a group comes out of the client in the player's language: the
    activity's fullName and shortName from C_LFGList, the server-rendered
    playstyleString, the four GROUP_FINDER_GENERAL_PLAYSTYLE globals -- read once
    at file load, so a nil there is nil for the session -- and the spell name that
    goes straight into the teleport button's /cast macrotext. Nothing here has
    ever seen a German one: tests/wow_mock.lua answers enUS throughout, and the
    /wg test fixture spells the activity name out in English, which § 12b says
    plainly so nobody files the fixture as a bug.

    § 12b is written from that enumeration, and each step names its failure: an
    Unknown in the Instance row where the client plainly has a name, a name that
    overruns the popup's border (German activity names are the longest the client
    produces, and nothing else would notice), a playstyle global that came back
    nil, a teleport button drawn ready whose click casts nothing.

    Step 5 is the point of scheduling this at all. Session 6 owns § 7a as well as
    the locale pass -- one login, both jobs -- and § 7a is the
    C_SpellBook.IsSpellKnown observation WHATGROUP-R-06 has been gated on since it
    was filed. M5-10 correctly refused to ship the rung without it. So § 12b
    carries a fill-in block for the six readings the finding needs, including the
    client build and GetLocale(), and states both outcomes explicitly: agreement
    means M5-10 can add the rung in the shape the five siblings use, disagreement
    means the rung is NOT added and the disagreement is recorded on issue #15.
    Both close session 6's obligation. A blank is the only result that does not.
    The checklist and § 13's standing note about 7a both now point at it.

    Nothing here has been performed. No WoW client was available to this
    execution, so § 12b is marked NOT YET RUN, no step in this repository is
    recorded as passed, and WHATGROUP-R-06 stays exactly where M5-10 left it.

    The gate lands with the section: a case in tests/test_doc_structure.lua
    asserting the document still carries a non-English-client section, that it
    names a client to run it on, that it says what failing looks like, that it is
    more than a heading with a sentence under it, and -- the one assertion
    peculiar to this repo -- that it names C_SpellBook.IsSpellKnown, so the single
    login this addon needs cannot be spent without § 7a. It was watched failing on
    the "has no heading naming a non-English client" message before the section
    was written, and it claims nothing about the section having run.

    luacheck . is 0 warnings / 0 errors in 41 files; lua tests/run.lua is 559
    passed, 0 failed, 0 skipped, 559 total. The count moved by the one new case,
    so docs/test-cases.md is regenerated and the README badge moves with it here.
    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 5f7272bbcdf3f3ac26ec515ac1b7b8de973e7ea7
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 19:21:38 2026 +0530

    M4c-04: the blanket ignore goes, and fifteen of the twenty-four were real

    `.luacheckrc:19` carried `ignore = { "211/addonName", "212", "542" }`. All three
    codes named something that exists in this tree, but the suppression reached all
    40 files, and `M4-11` is explicit about what that is worth: an ignore that
    silences the wall reads as coverage and provides none.

    It provided none here, and the measurement is in the commit. Removing the three
    lines took `luacheck .` from 0/0 to **24 warnings** — ten `211/addonName`,
    thirteen `212`, one `542` — and the more useful number is that **fifteen of the
    twenty-four were not conventions at all**:

      * Ten files opened `local addonName, NS = ...` over a folder name they never
        read. Six files in this addon do read it — CoreSetup, EnvSetup, MediaSetup,
        DebugLogSetup, WhatGroup and settings/Panel, each handing it to a vendored
        library that cannot infer which folder it was copied into. The other ten had
        it because the line was copied. They now open `local _, NS = ...`, which is
        what ConsumableMaster does and why its config carries no `211` entry at all.

      * The two file-load `hooksecurefunc` post-hooks mirrored the client's full
        signatures and forwarded them into handler bodies that read none of it.
        `OnApplyToGroup` took a `...` it never touched; `OnSetItemRef` took
        `linkArg, text, button, ...` and touched none of the four — by the time
        control reaches it the closure's prefix test has already answered the only
        question those arguments could, and there is exactly one `WhatGroup:` link.
        Both closures and both handlers now take only what they read. Deleted, not
        re-listed: an argument this addon chose to accept and then ignored is dead
        code, and a stanza naming it would have been the blanket again, smaller.

    Nine remain, and each is a receiver a calling convention forces. Three per-file
    stanzas in luacheck's `<code>/<variable>` form — `212/self` on the two capture
    methods reached as `self:Capture…`, on the four `ApplyFrame*` appliers that read
    the popup through modules/Frame.lua's own upvalue, and on the AceConsole handler;
    `212/event` on the AceEvent handler that is named after its own event — with a
    comment above each saying which obligation forces the argument. The single 542 is
    answered inline, `-- luacheck: ignore 542` on the `invited` branch line in
    core/WhatGroup.lua, because it is the only empty branch in the repository and a
    file-wide suppression would hide the next one.

    The narrowing is real, and it was measured rather than assumed: a dead second
    parameter added to `WhatGroup:RunTest` reports under the new config
    (`core/WhatGroup.lua:855:28: (W212) unused argument deadArg`) and reports
    nothing under the old one — the same tree, re-linted with
    `luacheck --config <the blanket> .`, comes back 0 warnings / 0 errors.

    The gate is the other half, and `M4c-03` in ConsumableMaster is 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, all four 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_docmap and test_eol
    already strike here.

    The working tree also went red on the way through, and that was the EOL gate
    doing its job: the first pass rewrote `.luacheckrc` through a text-mode filter
    and left it 114 LF terminators against the `eol=crlf` `.gitattributes` declares.
    Repaired in place before anything was staged.

    Documentation moved with the code. docs/data-flow.md quoted both pre-change
    signatures in its `SetItemRef` section and now quotes the shipped ones with the
    reasoning beside them; docs/ARCHITECTURE.md said "every source file starts with
    `local addonName, NS = ...`" in two places and now says which six spell it that
    way and why the other ten spell it `_`; docs/testing.md gains the suppression
    gate beside the lint-scope section it belongs to.

    Verification, observed. `luacheck .` 0 warnings / 0 errors in 41 files.
    `lua tests/run.lua` 558 passed, 0 failed, 0 skipped — 554 before, plus the
    gate's four. docs/test-cases.md regenerated through `--list` (which writes CRLF
    itself) and its Totals row reads 558; the README `[tests]` badge moves 554 to 558
    in this commit. Both re-run from a clean clone of this commit, not from the
    working tree. No version anywhere: Decision 5 stands and the badge is a count.

    No smoke step, and none is claimed. Two lines that reach the client did change —
    the parameter lists of the two file-load hook closures — and that is a signature
    narrowing with no behavioural edge: Lua drops arguments a function does not
    declare, which is already what happened to these. The paths are covered by
    smoke-tests.md sections 4.2 (chat-link round trip) and 5.1 (a real LFG apply),
    and tests/test_lifecycle.lua drives both hooks through `mock.fireHook`: four
    cases push the client's three `SetItemRef` arguments into what is now a
    one-parameter closure, and one pushes a search-result id at `ApplyToGroup`. Nothing else
    in the change ships: `.luacheckrc` and tests/test_lintconfig.lua are not packaged,
    and the ten header edits rename a local nothing read.

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

commit d54a8dc9f14aaff667dde8bbcde247a8e55a3db7
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 18:32:42 2026 +0530

    M5-01: the record is regenerated, and the band table stops being empty

    The watch list this replaces said "No file is within 250 lines of the 1000-LOC threshold."
    `tests/test_frame.lua` is 1063 — the first file this repository has ever had in the
    `layout-§1` band, and now the largest file here, ahead of `core/WhatGroup.lua` at 870. It was
    627 at the previous run's commit, crossed at 987 with `M2-21`'s combat-start re-ask, and
    reached 1063 with `M2-28`. It is 293 lines longer than the 770-line module it covers, and that
    ratio rather than the count is what its disposition says to watch.

    The staleness was structural, not careless. Until test-kit revision 15 the runner wrote one
    table row and a fixed lead-in, so `automated-tests-§4`'s two tables had no producer and the
    only way to have them was by hand. Everything in `RESULTS.md` is now the runner's, out of this
    run's own manifest and `lizard` output, with Disposition the one authored column.

    Green. Lint 0/0 over 40 files — 16 before `M4-11` put the test tree in scope — 554 cases passed
    with none failed and none skipped, zero functions above CCN 15. `perf` is a permanent skip, no
    `tests/perf.lua` here, so the record says nothing about runtime cost rather than implying it.

    Totals rose 21% and 15% against `20260825-103505`, the largest proportional growth in the
    collection this cycle, with the averages moving a decimal each. The ceiling has not moved at
    all: `WhatGroup:LFG_LIST_APPLICATION_STATUS_UPDATED` is still exactly 15, reported at `@759-821`
    now against `@634-697` when it was named — the function slid down the file and its score did
    not budge. It is one event handler doing one job, with no seam of the kind that retired the
    four entries before it, and the next branch added to it blocks a tag.

    The regeneration dropped the hand-written history, and the bundle's ANALYSIS.md keeps it,
    because it is the most useful thing this record has ever carried: every entry this watch list
    has ever held was retired by splitting the function and not one by renewing an accept, four of
    four. So is the diagnosis that went with the last of them — `lizard` scores Lua closures as
    separate functions, so the two nested closures contributed nothing to `ConfigureTeleportButton`'s
    20; it was twelve genuine top-level branches, and the extraction order proved it.

    `20260807-110421` and `20260825-103505` 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 e735453c80869ba5e5b58fdc2332ee1c67a769e3
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 18:00:48 2026 +0530

    M5-02: a row that recorded its own expiry, and a section number that never existed

    The register carried two `performance-§12` rows. The first claimed the
    no-combat-path exemption on 2026-08-02; the second recorded that the first's
    re-check trigger had fired on 2026-08-06, when `modules/Frame.lua`'s teleport
    cooldown got a repeating ticker. The first row said so of itself, in its own
    trigger cell — **FIRED 2026-08-06** — and was kept "only as the record of what was
    claimed and on what evidence".

    That is a graveyard entry, and it is the case `audit-review-history`'s new third
    MUST was written against: an audit must evaluate every trigger against the tree
    and report any row whose condition has already come true, because that deviation
    ended on the day the condition came true and every day after it the document
    asserts a live deviation that is not one. This one asserted one for a month. So
    the row goes, and nothing is lost with it: the claim and the date it ended are now
    the opening sentence of the row that survives, which is where a reader looking at
    today's deviation is actually standing, and the evidence is where it always was —
    issue #7 and `performance.md`'s regenerated sweep. The surviving row's heading and
    its first trigger are reworded so neither points at a row that is no longer there.

    `standalone-windows-§33` resolved to nothing. `standalone-windows.md` carries one
    subsection heading and no numbered `§N` subsections at all, so §33, and §29 and
    §32 in the same row's body, were three citations into a numbering scheme that file
    does not use. The decision itself is sound and stays: the footer *Close* keeps its
    label without a mark, because the SHOULD is about buttons whose action lands
    somewhere outward and dismissing a dialog is neither outward nor ambiguous. What
    changes is that the row now cites the section by bare filename and quotes the
    SHOULD's own words in *What differs*, so the claim can be checked against the rule
    it claims to depart from — which `documentation-§3` requires and a paraphrase
    cannot do. `WG-52`.

    The preamble gains the shorthand's expansion. This repository writes review
    findings as `WG-R-NN` — in the register, in `core/WhatGroup.lua`, in
    `modules/Frame.lua` and in two suites — and `docs/reviews/2026-09-07/` assigns
    them as `WHATGROUP-R-NN`. Saying so once makes every use of the short form
    resolvable without renaming a convention spread across the repository, which is a
    decision for its own commit.

    `tests/test_register.lua` is the gate over the mechanical half: slice
    `## Documented deviations`, pull every `WG-`-shaped deviation id out of it and
    require each to be ASSIGNED by a bundle under `docs/audits/` — heading a table
    cell, a heading or a bullet, not merely quoted, because a bundle reporting a dead
    citation quotes the dead id while doing so. The `-R-` shorthand is skipped
    deliberately and the file says why. Watched go red on a planted `WG-99`.

    luacheck 0/0; 554 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, 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 3a561580031ee4a6b5ae3a1af54f83a6df64f9aa
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 17:51:17 2026 +0530

    M5-03: the README stops naming a settings tab that does not exist

    Two README shapes documentation-§1 fixes, and one of them was a lie a player
    could act on.

    `## Version History`'s 1.3.0 row said "add a delay under Notify". There is no
    Notify tab. `notify` is the row's SECTION — where the value is stored and how
    `/wg list` groups it — and `settings/Schema.lua` files `notify.delay` under
    group **Chat**, which is the tab it is edited on. The same feature's
    `## What's new` bullet twelve lines above already said Chat, so the README
    contradicted itself and the wrong half was the one a player scrolls to when
    looking for what changed. Corrected to Chat.

    The `### Settings panel` table was headed **Tab | Covers** with three rows for
    this addon's one settings subcategory. §1 asks for one row per PAGE and puts the
    per-tab breakdown in docs/settings-panel.md, which already carries it under
    `## The tab strip` with every row and where it is stored. So the three rows
    collapse into one `| Page |` row naming the three tabs in a sentence each, with
    a link to that section. The per-panel prose under it stays — §1 explicitly
    allows it where a panel is rich enough, and this one is.

    Two of the gate's seven cases are this repository's. The tab case reads the
    declared tab names out of settings/Schema.lua rather than restating them —
    plus `Master controls`, whose rows are composed by LibKa0s and carry no
    `group =` line here at all, and whose name options-ui-§15 mandates — and then
    checks every "under <Tab>" the README uses to point a player somewhere. It
    splits a bold breadcrumb at the arrow, because `**Chat → Notification Delay**`
    names a tab and then a row on it and only the first half is a tab. The other
    five are the collection's shared doc cases: the ten mandated hub sections, the
    60-line spill threshold over all but the two registers, the anchors into the
    hub, the two allowed history surfaces, and the README's sections in §1's order.

    Watched both red in the working tree first. With the old README restored the tab
    case names `Notify` and nothing else, and the table case quotes the
    `| Tab | Covers |` header back.

    546 -> 553 — docs/test-cases.md regenerated and the badge moved here. luacheck
    0/0 over 39 files, 553 passed, 0 failed. No version touched.

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

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

    M5-04: docs/compat-layer.md, and the row that called seven shims none

    The Tier 2 row read "core/Compat.lua normalizes LFG and unit APIs with no
    addon-specific shim to document separately". There are seven, at :24, :40, :52,
    :62, :83, :105 and :125, and documentation-§3's threshold is three. The row was
    not a judgment call that went the other way; it was a description of a file
    nobody had counted.

    The doc covers the seven as three groups and spends its length on the parts a
    reader gets wrong. Why GetSpellName falls through when the modern API is present
    and answers nil rather than only when it is absent — the client can carry
    C_Spell.GetSpellName and still have nothing to say about an uncached spell, and
    going quiet there blanks a label the deprecated reader would have filled. Why
    the two cooldown readers are two: the remaining-seconds one floors at the GCD,
    because without it casting anything at all makes an eight-hour teleport report
    "on cooldown" for a second and a half, and the swipe one floors at nothing,
    because a swipe is a shape rather than a sentence. And why every default is what
    it is, chosen from what the caller does with the answer rather than from habit.

    IsSpellKnown gets its own section, because it is the one accessor with no modern
    rung and that is recorded rather than fixed. WHATGROUP-R-06 conditions the
    C_SpellBook rung on an in-client observation that both APIs agree, smoke-tests.md
    § 7a is that observation, and it has not been run — so the page says so, and says
    what the failure looks like when it comes: every teleport desaturated with
    "Teleport spell not learned" on a character who has learned all of them.

    tests/test_docmap.lua holds the map to the directory: Present names a file that
    exists, Not applicable names one that does not. Whether a trigger has fired stays
    prose and stays a human's to judge; the status is not prose. Watched it fail with
    the doc moved aside; it named the row.

    545 to 546 cases; docs/test-cases.md and the README badge move with it. No
    version bump, no CHANGELOG: 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 e0a7c5c80569b30505604d39a09dff3d73bc12f6
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 17:21:25 2026 +0530

    M5-10: the IsSpellKnown rung waits, and the wait is now written down

    WHATGROUP-R-06 asks for a C_SpellBook.IsSpellKnown rung above the bare
    global in core/Compat.lua:62-67, so the one accessor of six with no modern
    rung matches its five siblings at :24, :40, :52, :83 and :105. It does not
    ask for it unconditionally. The finding's own fix text says the rung goes in
    "only once an in-client check confirms both APIs present and agreeing", and
    06_SMOKE_TESTS.md § 6.3 is that check.

    No WoW client is available to this execution. The check has not been made,
    so the rung does not land. Writing it anyway would be manufacturing the
    evidence the finding gates itself on, which is the one thing the finding
    explicitly refuses. core/Compat.lua is untouched.

    What lands instead is the step, in the place this collection keeps steps
    that need a login: docs/smoke-tests.md gains § 7a under patch-day smoke,
    which is where the shim's failure would surface. It names the two dumps, the
    learned and unlearned spell, the pass reading and the fail reading, and it
    says what a fail means — if the two APIs disagree they are not
    interchangeable, and the answer is a decision rather than a fallback ladder.
    It also says what the headless suite can and cannot see: test_compat.lua:143
    already nils the global and asserts false, which proves the shim degrades as
    designed and proves nothing about whether false is the right answer on a
    client that knows better.

    The symptom is worth naming because it is loud. If Blizzard retires the
    global, every teleport draws desaturated with "Teleport spell not learned"
    beside it on a character who has learned all of them, and every chat row is
    tagged (not learned). That is why the step is filed under patch day and not
    under a one-off.

    The finding is on the books as issue #15, state:untriaged, carrying the
    blocked state and the two outcomes so the next person with a client knows
    what to record and which of the two answers they are allowed to write.

    luacheck 0/0, 545 passed 0 failed. No test count moved, so no test-cases.md
    and no badge. No version 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 ef2247cb924bdfa29ac17b2ba7b6ee8dc05bbab7
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 14:35:29 2026 +0530

    M4-24: the exemption's evidence points at today's code

    The plan's WhatGroup share of this item reads "amend the register row M2-21
    makes accurate", and that half was already done: M2-21 amended it in its own
    commit rather than deferring it here, so `ARCHITECTURE.md`'s live
    `performance-§12` row has said "enforced rather than asserted" since 2026-09-08.
    Reporting the item done on that basis would have been wrong, because the item's
    acceptance is not the sentence -- it is that running the page's own grep
    reproduces the table printed on it and that every `file:line` resolves. Neither
    held.

    Ten of the twelve call sites the sweep table cites had moved. Two are stale in
    the ordinary way -- `settings/OptionsSetup.lua:219`, `:227` are now `:239`,
    `:247`; `core/WhatGroup.lua:625` is `:638`; the three `modules/Frame.lua`
    citations are `:351`, `:394`, `:733`. One had drifted in the way that actually
    misleads: the row for `PLAYER_REGEN_DISABLED` cited `core/WhatGroup.lua:193`,
    and `:193` still holds a `RegisterEvent` -- for
    `LFG_LIST_APPLICATION_STATUS_UPDATED`. A citation that lands on a plausible
    wrong line is worse than one that lands on a comment, because the reader
    checking it stops checking.

    The larger defect is substantive and it belongs to this cluster rather than to
    line drift. `M2-28` established that `f` parents a `SecureActionButtonTemplate`
    child, so the client refuses `Hide` on `f` under lockdown, and routed every hide
    through one `hidePopup()` seam that returns false instead of calling into the
    refusal. It did not update this page. So the sweep went on crediting the
    `PLAYER_REGEN_DISABLED` row with "at most one `Hide`" -- the single thing that
    edge cannot do -- and the `PLAYER_REGEN_ENABLED` row with "at most one `Show`",
    omitting that the leaving edge is where every deferred hide lands, including a
    `Close` the player pressed mid-fight that outranks the gate. Both cells are
    rewritten to the asymmetry the code has: the entering edge can only show, the
    leaving edge carries both directions. `ARCHITECTURE.md`'s event-table cell said
    the same false thing one table above the register row and is corrected with
    them.

    The rows are now in the order the grep prints them, so a reader can hold the two
    side by side, and the page reconciles its raw output rather than leaving it to
    be inferred: seventeen lines, five of which are the pattern names appearing
    inside comments, twelve call sites, eleven rows. That reconciliation is the
    thing that makes a future drift visible instead of arguable.

    The per-tick cost gains its third item. The ticker calls
    `C_Spell.GetSpellCooldown`, builds a `NS.FormatDuration` string, and calls
    `SetText`; the page, the register row and the ARCHITECTURE prose all said two of
    those three. The string build does not change any conclusion, which is exactly
    why it should be written down rather than rounded away.

    The exemption is NOT withdrawn and is not re-litigated. It still rests on
    criteria (b) and (c) with (a) recorded as fired on 2026-08-06, both register
    rows keep their decided dates, and both re-check triggers are untouched. No Lua
    changed.

    `docs/frame.md` and `docs/module-map.md` cite the same timer as the thing that
    ended the exemption, so their `:312` moves to `:351` here rather than being left
    as the last two copies of a number this commit retires.

    Deliberately not touched, and not fixed by stealth: the
    `events-frames-taint-§8` register row cites `core/WhatGroup.lua:515`, `:523`,
    `:531` for the join summary's gold-labeled rows and those have moved too, and
    `docs/slash-dispatch.md` cites `core/WhatGroup.lua:180` and
    `settings/OptionsSetup.lua:250`. They belong to a different deviation and a
    different page, each with its own evidence, and repairing them from inside this
    item would mean re-deriving claims nobody has re-read. They are reported as
    follow-up.

    Documentation only. luacheck 0/0 in 37 files, 545 passed / 0 failed, and the
    `--list` inventory still matches `docs/test-cases.md` byte for byte -- no case
    moved, so no `[tests]` badge movement. No smoke step: there is no runtime
    behaviour here for a client to exercise, and the combat-edge asymmetry this page
    now describes is already covered by `M2-28`'s smoke step 4.5, still NOT RUN.

    No version moves. 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 3d252f0342a5b424ab4dba8c6cda4e3f6818cbb6
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 14:26:20 2026 +0530

    M4-19: two sleeping cases wake up, and the third finding was never asleep

    WHATGROUP-R-05, R-10, R-11. C21's rule is that a case which cannot go red is
    worse than no case, because it reads as coverage. Three were named here; two
    were real and are fixed below, and the third was a grep artefact.

    WHATGROUP-R-05 is not-needed, and the proof is worth more than a change would
    have been. The finding says "grep finds nothing pinning the suite list itself".
    Nothing in tests/ does, and nothing needs to: tests/_kit/framework.lua:885-887
    has Kit.run call Kit.assertSuiteInventory(dir, suites) itself whenever `dir` is
    given, before a single suite is loaded, and tests/run.lua has always passed
    `dir = "tests/"`. Both directions were watched red in the working tree:

      deleting the "test_util" entry -> exit 1, "tests/test_util.lua exists but is
      not declared in the suites list ... it is running zero cases today"
      renaming tests/test_util.lua  -> exit 1, "tests/test_util.lua is declared in
      the suites list (position 6) but is not on disk"

    That was already true before M4-01 re-vendored the kit -- the pre-M4-01
    framework.lua carries the same call at :677 -- so this was never a gap, at
    triage time either. Four sibling repos additionally call assertSuiteInventory
    from a named case in test_harness; adding a fifth here would raise the case
    count for its own sake, which C21 lists as a non-goal, and would fire strictly
    later and quieter than an abort before load. So what lands is what 03_SPEC.md
    gives LibKa0s for this same item: a comment naming the pin. The reason that is
    not nothing is that this pin is INVISIBLE from the runner -- which is exactly
    how a reviewer with a grep concluded it was absent, and how the next reader
    would pin it a second time.

    WHATGROUP-R-10 was real. "ships no private copy of the shared art" stat'd one
    literal path, media/fonts/JetBrainsMono-Regular.ttf, so the only arrangement it
    could ever see was a re-added copy of the same face under the same name. A
    different face, the same face renamed, or a private duplicate of a catalog mark
    all walked past a green case whose title promised to have looked. It now scans
    every path git tracks under media/ for any .ttf/.otf and for any basename in the
    library's ICONS catalog. Tracked rather than on-disk is deliberate: what ships is
    what the repo carries. It fails rather than passes when it cannot look -- no
    io.popen, a dead pipe, or an empty listing is a FAIL, because "no result" and
    "nothing found" are the same string and only one of them is good news. Seen red
    under both mutations its comment names, quoted there verbatim.

    WHATGROUP-R-11 was real and had two halves. SEAM_FILES named four paths, the
    four files that built descriptors the day it was typed; a fifth file growing a
    `lib:New` descriptor was swept by nothing, forever, in silence. It is now
    T.loadAddon.tocFiles -- the same derivation tests/loader.lua feeds the sandbox,
    so the swept set and the loaded set cannot drift. Seen red with
    `local d = { L = NS.L }` appended to core/Util.lua, which the old four-path form
    reported PASS over. The `if src then` that wrapped the assertion is gone in
    favour of assertTrue(src ~= nil).

    What a reader would otherwise get wrong about that last line: it is a guard, not
    a gate, and I did not see it fail. Once SEAM_FILES is TOC-derived, a path that
    does not resolve kills tests/loader.lua's loadfile at runner start -- I checked,
    and `mv core/Util.lua` aborts the whole run before any case executes. So the
    unreadable-path red is real but belongs to the loader, not to this assertion.
    It stays because the sweep must never be able to narrow in silence whatever the
    list is derived from later, and the comment says so rather than claiming a red
    nobody has watched.

    luacheck 0/0 over 37 files; 545 passed, 0 failed -- unchanged either side, so
    docs/test-cases.md and the README [tests] badge do not move, and `--list`
    diffs clean against docs/test-cases.md. No smoke step: nothing outside tests/
    changed and 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 12619141562c1421e3e8058c2f3b92ad7bff6134
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 14:12:37 2026 +0530

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

    WG-47 / WHATGROUP-A-02. `toc-file-§5:144` binds load-bearing positions only, and
    the collection audit that read it against all 23-81 lines of nine TOCs and
    concluded the rule was unworkable was counting against the wrong denominator.
    `M1-STD-02` settled that upstream; measured against its own denominator this
    file had four unannotated load-bearing positions, and now has none.

    What the reader would otherwise get wrong: the `# Core` block was already
    annotated in three slots, which made the silence below it read as *examined and
    free* rather than *never examined*. It was the second. All four constraints are
    satisfied today by accident of the order someone typed, and all four fail the
    same way if a future reorder is done from the file listing alone -- silently,
    only in the client, with no test going red:

      - `core\DebugLogSetup.lua` builds the console with `lib:New` at file scope
        (`core/DebugLogSetup.lua:120`) and reads `NS.FONT_MONO`, which
        `core/WhatGroup.lua:121` assigns at load. The library validates `font` as a
        string, so above that line the console is built from nil.
      - `defaults\Profile.lua` publishes `NS.C`, which `settings/Schema.lua:27`
        takes as a file-scope upvalue and dereferences in every `add{}` at load.
      - `settings\Schema.lua` creates `Settings.Schema`; `settings/Panel.lua:268`
        splices the master rows into it at file scope.
      - `settings\OptionsSetup.lua` publishes the LibKa0s-Options instance as
        `Settings.Helpers` (`settings/OptionsSetup.lua:273`); `settings/Panel.lua:204`
        calls `Helpers.MasterControls` at file scope. `Panel.lua:193-195` already
        stated this dependency -- in the Lua file, which is not where the rule wants
        it or where the person moving the TOC line is looking.

    `settings\Panel.lua` itself carries nothing and is compliant: its position is
    pinned by the two annotated lines above it, and `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.

    The three group headers also pick up the *conventional* SHOULD note, once per
    group, which is the reading `M1-STD-02` settled and `AbsorbTracker.toc:36-37`
    is the only prior instance of. `# Settings` carries one extra fact: the group
    as a whole must follow `# Core`, because `settings/Slash.lua:67` builds
    `CLI_MISSING` from `NS.LIBKA0S_MISSING` at file scope. That is a real load-time
    read the audit did not name, but it is a cross-group constraint pinned by
    `core\CoreSetup.lua:34` being first in an already-annotated block, so it belongs
    in the group note rather than as a fifth per-line MUST.

    Comment-only. No file entry moved, no order changed, no behaviour changed.
    No new smoke step: the client-side surface here is that the TOC still parses
    and still enumerates all fourteen addon files, which is exactly what §1.1 Cold
    load covers and it is already flagged NOT YET RUN for session 3. A step
    asserting a comment is a comment would be noise.

    Verified: `luacheck .` 0 warnings / 0 errors in 37 files; `lua tests/run.lua`
    545 passed, 0 failed; `--list` diff against `docs/test-cases.md` empty. The case
    count does not move, so no `docs/test-cases.md` or `[tests]` badge change. 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 5abc522d28a4b0b12b6c16e8610585dbed3461a9
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 14:06:05 2026 +0530

    M4-11: the test tree comes into lint scope

    `.luacheckrc:9` excluded bare `tests/`, which is what `lint`'s own template shipped until
    `M1-STD-01` amended it, so all 21 of this repo's own test files sat outside the gate. `luacheck .`
    covered 16 files and reported 0/0; it now covers 37 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.

    `_dev/` joins the list in the same line, closing `WHATGROUP-A-14`. It does not exist in the tree
    today and that is exactly why it belongs there: `.pkgmeta:12` already reserves it whether or not it
    exists, and a scratch directory that lint would happily read on the day someone creates one is a
    gate that disagrees with packaging about what is and is not this addon's code. Both config files
    now say the same thing about it.

    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 at the top level is a permission granted
    to `core/`, `modules/` and `settings/` as much as to a suite, and a shipped file reaching for the
    test harness is precisely what lint is here to refuse. It is spelled `_G.WHATGROUP_TEST` and not
    bare, because every suite reads it through `_G` and `tests/run.lua:75` is the writer —
    `globals`, therefore, not `read_globals`. `_G.WhatGroupDB` is named beside it as a field for a
    narrower reason: `tests/loader.lua:91` sets it to nil before each boot, because the kit's AceDB
    fake resolves a SavedVariables name against the real `_G` and would otherwise hand the next
    instance the previous one's saved table. Its bare name stays writable above, for the shipped files
    that own it.

    Eight findings came out of the newly-linted files and all eight 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.

    Five were dead bindings, and each one is a reader's trap rather than a style point.
    `tests/test_database.lua:25` and `tests/test_settings.lua:120` both re-destructured assertions off
    `T` mid-file: the first bound an `assertNil` nothing below it calls, the second re-bound an
    `assertTrue` already bound at `:3`, so a reader hunting for where an assertion comes from finds two
    answers. Three suites — `test_frame.lua:126`, `test_slash.lua:80`, `test_util.lua:213` — took the
    third return of `T.bootAddon()` and never touched it. In a harness where taking `mock` is how a
    case says "I am going to assert on what the client was told", an unused `mock` reads as an
    assertion someone forgot to write. Dropping it says the case genuinely asserts through `env` and
    the addon's own state.

    The last three are one bug with three warnings. `tests/wow_mock.lua:494` named the AceGUI receiver
    `self`, and the three widget methods defined inside it — `SetRelativeWidth`, `SetSliderValues`,
    `ReleaseChildren` — each take their own implicit `self`, which is the WIDGET, not the library. Two
    different tables under one name in one scope is how a mock grows a defect that reads as correct on
    every line; renaming the outer parameter to `lib` costs nothing, since it is passed straight
    through to `baseCreate` and never read again.

    `docs/testing.md:275` said the quiet part out loud and was made wrong by this commit: it told a
    reader that `tests/` is excluded, which was true when written and is now the opposite of the rule.
    It is corrected in the same commit, and states which single directory inside the tree is 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.

    No case count moves: 545 passed, 0 failed, before and after. `docs/test-cases.md` and the README
    `[tests]` badge are therefore untouched, and `--list` still matches the document byte for byte.
    Nothing here is reachable from the client — the lint config is not shipped and the test tree is not
    loaded in game — so this item adds no smoke step.

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

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

    M4-09: the stub-parity gate becomes a suite, and names its live half

    The plan's premise for this repo is stale and the correction matters more than
    the change. 04_EXECUTION_PLAN.md's row says six of the nine "have nothing today",
    and 01_CONSOLIDATED_FINDINGS.md:153 says the six have "nothing checking the stub
    against the live surface". That was true when the audit was triaged; it stopped
    being true on this branch, at 7e4813a, which added four Kit.assertSurfaceParity
    cases covering all four adopted seams. So nothing here closes a coverage gap. What
    lands is a MOVE and a FORM change, and the form change is the part with a future
    in it.

    The four cases leave tests/test_libka0s.lua for tests/test_surface_parity.lua,
    which is the filename the spec's acceptance names in all nine. Case count does not
    move -- 545 either side -- so the README badge does not move; docs/test-cases.md
    does, because the inventory groups by suite file, and it is regenerated here.

    The three LIBRARY-BACKED cases move onto Kit.assertSurfaceParity's by-name form,
    which arrived with kit 15 in M4-01. The four-argument form walks every key of the
    live table, so the Options case had to exempt `__print` by hand -- one line the
    v1.27.0 re-vendor added, and one that would have been joined by another on the
    next re-vendor that grew an internal. The by-name form compares Kit.publicMembers,
    which drops the whole `__` prefix, so those exemptions are the kit's rule now
    rather than this file's typing. libs/LibKa0s/Options.lua's own comment at O.__print
    already states that rule; the case now enforces it instead of restating it.

    Core does NOT move onto the by-name form. It is not a major's surface as this
    addon consumes it: core/CoreSetup.lua hangs its members on NS itself, so the
    namespace IS the seam's surface and there is no name to look a live half up under.

    THE LOAD-BEARING HALF IS ONE TABLE IN tests/run.lua, and it is what a reader
    would otherwise get wrong. Kit.expose auto-wires a LibStub off the exposed table,
    which is correct for a repo whose stubs mirror LIBRARY TABLES. This runner exposes
    factories rather than a built addon, so nothing is auto-wired at all -- and all
    three of this addon's library-backed stubs mirror an INSTANCE, what
    lib:New(descriptor) returned, so even a reachable LibStub would answer the wrong
    thing ("LibKa0s-Options-1.0" is a four-member table: LAYOUT, New,
    PatchAlwaysShowScrollbar, STRINGS). Watched fail in the working tree with the
    suite declared and the registration absent: all three red with the kit's own
    "no surface source is registered ... this gate cannot run", which is its
    deliberate bargain -- an unresolvable name reddens rather than quietly passing.
    That is where AbsorbTracker's M4-09 differs from this one: there the auto-wiring
    fires and resolves the wrong table, here it never fires. Same fix, different red.

    The registration costs ONE extra addon load at runner start. It is read from and
    never handed to a case, so the per-case isolation the factories exist for is
    untouched -- and it is why this file's live half carries no test seams a previous
    suite stamped on, which is an exemption AbsorbTracker's shared-instance runner has
    to carry and this one does not. Set BEFORE Kit.expose, which is what makes it
    stick: expose registers a source only when none is registered yet.

    The degraded arm's file list is now DERIVED, as T.loadAddon.libFiles, instead of a
    second hand-typed copy of libs/LibKa0s/LibKa0s.xml's load order. A file added to
    the vendored library joins the skip on the re-vendor commit rather than on the day
    somebody notices (anti-patterns #48). The copy in test_libka0s.lua stays because
    its other degraded cases still use it.

    ONE CONSEQUENCE WORTH KNOWING BEFORE READING THE STUB. The twelve `__` members in
    settings/OptionsSetup.lua's degradation arm are no longer compared by anything --
    that is exactly what the `__` filter means. Their comment claimed the parity case
    compared the whole set, which was the reason they were there; it is corrected in
    this commit to say plainly that the reason is now shape alone and that shape is a
    weaker reason than a gate. They are kept rather than deleted because deleting
    shipped lines was not what this item was for; the decision is stated so the next
    person makes it deliberately instead of inheriting a dead justification. The two
    other stale citations in that file, at the tabbed-page block and the composer-data
    block, are repointed at the new filename in the same commit.

    No smoke step. The row carries no warning mark and its verification is a headless
    mutation, so nothing here schedules client time.

    Verification. luacheck 0/0 in 16 files. `lua tests/run.lua` 545 passed, 0 failed,
    unchanged either side. `--list` diff against docs/test-cases.md empty, CR-normalized
    both sides. The new file is CRLF and `git ls-files --eol | grep -E 'w/(lf|mixed)'`
    still returns only the eol=lf shell script. Each of the four cases watched RED in
    the working tree under a planted mutation and green after revert:

      Core     -- deleting `function NS.MakeCloseButton() return nil end` from
                  core/CoreSetup.lua's degraded arm gives "MakeCloseButton is missing
                  (live: function)"
      DebugLog -- deleting `Clear` from core/DebugLogSetup.lua's stub names Clear
      Options  -- deleting `H.SetRenderer` gives "SetRenderer is missing (live:
                  function)"
      Slash    -- degrading `Text` to `false` gives "Text is a function live but
                  boolean degraded", which is the arm a present-but-not-callable
                  member falls into and the one a plain "is the key set?" check
                  waves through

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

commit c6b6c3af0390cfa3902b8ebf51a0965ef9279add
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 13:09:59 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
    WhatGroup's authored text: source comments, test case names and assertion
    messages, and every live doc.

    The audit named three comments — modules/Frame.lua:267, settings/Schema.lua:115,
    settings/OptionsSetup.lua:92. Two still sit where it said. The Frame.lua one is
    now at :333, because M2-21 and M2-28 both added lines above it; the comment is
    the same comment. Against the full published list the real figure is 51 lines
    across 22 files, and the difference is entirely the entries a colour|behaviour|grey
    grep cannot see: honoured, neighbouring, favour, labelled, modelled, modelling,
    acknowledgement, and the two uppercase MODELLED / UNMODELLED in the mock.

    Three spellings stay, and they are the section's Blizzard-symbol exception rather
    than misses. `cancelled` is an LFG application status the server sends, so
    core/WhatGroup.lua:727's APPLICATION_ENDED key, the literal handed to
    LFG_LIST_APPLICATION_STATUS_UPDATED at tests/test_capture.lua:433, and
    docs/data-flow.md:56's transcription of the four status strings all keep the
    spelling Blizzard uses. Correcting any of them would make the arm stop matching,
    silently — the capture would sit in the tables until the next group-leave, which
    is exactly the defect WG-R-07 fixed. The prose around them is swept.

    The audit also said no player-facing string was affected. Re-checked against the
    wider list and that holds: every hit outside the three above is a comment, a doc
    line, or a test name.

    Four case names moved, so docs/test-cases.md is regenerated here. The count does
    not move, so the README [tests] badge stays at 545.

    The plan's verification grep still returns one line, in docs/revendor/ — a frozen
    dated bundle, which 03_SPEC.md names as a non-goal. Add --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 a8ef42c908360bb375fe6f6225784e550b72bc24
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 11:43:00 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 the suites list with its own `dir`. 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. `grep -rn "__print" core modules settings` is empty here, so it
        is live-only in exactly the sense the rest of that list is. The library's own
        comment claims a stub need not mirror it because `Kit.assertSurfaceParity`
        skips the `__` prefix; that holds for the kit's BY-NAME form, which filters
        through `Kit.publicMembers`, and not for 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 § 12a, named in
    the re-vendor sentence at the foot of § 12 and in the quick-reference checklist.
    NOT claimed here. This addon hands its whole strip to `RenderTabbedSchema` and
    measures no band of its own, which is exactly why it can say nothing about one
    out of game.

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

commit 7c8df489cc7b813588fe70ebf4cff505038e9222
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 11:04:38 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.

    The root CLAUDE.md's one-line summary of the gate said flatly that a non-empty
    content diff is a real fork. That is true against the claimed tag and false
    against the sibling's working tree, and it is the working tree the command it
    quotes actually reads — so it is corrected here too rather than left as the short
    version that contradicts the long one.

    Verified here: luacheck 0 warnings / 0 errors in 16 files; 544 passed, 0 failed, 0 skipped, 544 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 caf1cbdb05717e90a7abf287725dc0dae211c41f
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 10:04:57 2026 +0530

    M3-05: re-vendor LibKa0s v1.26.0

    The last of the five, and the same shape as the other four: both vendored
    payloads copied whole from the tag, CLAUDE.md's provenance line rolled in the
    same commit, no WhatGroup code touched. 544 cases before, 544 after.

    WhatGroup consumes MasterControls and ColorPair and asks the composer for no
    font, border or bar texture, so OptionsCompose minor 3 -- the empty-dropdown
    fix this release exists for -- has nothing to repair here and travels because
    the library ships whole. OptionsWidgets minor 14 is the piece that matters to
    this addon: the tab strip acquires its buttons and content panel from a pool
    rather than building a set per click, and this settings panel draws one.

    Take the tag, not the sibling checkout. ../LibKa0s is at v1.27.0 already, so
    `diff -r ../LibKa0s/LibKa0s libs/LibKa0s` answers a question nobody asked;
    `git -C ../LibKa0s archive v1.26.0 LibKa0s testkit` is the reference, and it is
    the right one because archive runs the content through eol=crlf and reproduces
    the CRLF bytes on disk, which `git show` does not. Both diffs against it are
    empty -- content with --strip-trailing-cr and bytes with plain `diff -r`.

    The provenance line moved before the bytes so the gate could be seen refusing
    the mismatch: 543 passed, 1 failed on "libs/LibKa0s is the LibKa0s release
    CLAUDE.md says this addon bundles". That line is an input to
    tests/test_vendor_sync.lua rather than documentation about it, which is the
    whole reason the two have to land together.

    tests/_kit is untouched by this release -- Kit.VERSION is 14 at v1.25.0 and at
    v1.26.0 -- and was recopied anyway, since the kit and the library are
    re-vendored as one act.

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

commit d983ab2c7997d0fc38fc89b6c6cc152cdd36c327
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 02:24:47 2026 +0530

    M2-28: the popup cannot be hidden in combat, so stop asking

    Reported from the client: "AddOn 'WhatGroup' tried to call the protected
    function 'WhatGroupFrame:Hide()'", raised by pressing Close during a fight.

    buildFrame parents a SecureActionButtonTemplate button to f. The client refuses
    Hide on a protected frame and on every ANCESTOR of one while in combat, so
    f:Hide() is protected from every call site -- the Close button, ESC, and the
    visibility gate alike.

    modules/Frame.lua said the opposite in as many words: "this addon has always
    taken f:Hide() as unprotected, and the alternative is a window the player cannot
    dismiss". That premise was false, and it is the whole defect. ApplyFrameSize and
    ResetFramePosition sit two functions away and are both combat-guarded, both
    citing the secure child; the visibility seam was the one place that reasoned the
    other way.

    M2-21 built on that false premise and made it worse. It routed the gate onto
    PLAYER_REGEN_DISABLED, so entering combat with the popup up and visibility set
    to "out of combat" fired a refused Hide automatically on every pull, where
    before it took a deliberate Close press.

    Every hide now goes through one hidePopup() seam that returns false rather than
    calling into a refusal. A Close pressed in combat is remembered and honoured at
    PLAYER_REGEN_ENABLED, and it outranks the gate on that edge -- without that the
    player's own dismissal is overwritten by the value that permits the popup. The
    locale already carried "Popup deferred until combat ends." for this shape.

    What the player gets is asymmetric, and the comment now says so rather than
    claiming symmetry:

      inCombat    hides when combat ENDS   -- legal, honoured on the edge
      outOfCombat hides when combat STARTS -- refused, honoured one edge late

    The popup genuinely stays up for the fight under outOfCombat. There is no way
    around that while the secure child exists, and attempting it is strictly worse
    than deferring: the frame does not go down either way and the player gets a red
    error naming this addon.

    The reason 528 cases stayed green through all of this is that tests/wow_mock.lua
    modelled combat but not protection, so it answered "fine" to the exact call the
    client blocks -- and M2-21 extended that mock without noticing. The mock now
    tracks the parent chain, marks Secure* templates protected, and records every
    Hide the client would refuse in mock.blocked. Four cases assert that list stays
    empty; all four were seen red against the unfixed Frame.lua and green after.

    Two existing cases asserted the popup hides on the combat edge. The assertions
    were wrong rather than the code, and one of them tested the refusal path while
    believing it tested the re-show, so it now drives the re-show off `inCombat`
    where the hide is legal.

    544 passed / 0 failed (was 541), luacheck 0/0 in 16 files. Smoke step 4.5 added
    and NOT RUN -- no client here, and it is the only check that can see the real
    error.

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

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

    M2-22: pair a capture to its own application, not to whichever answers first

    The apply hook has a searchResultID and no application id. The status event has
    an application id and no searchResultID. Nothing in the client hands you both,
    so the code had to pick a join, and what it picked was arrival order: applies
    went onto the tail of a FIFO and every "applied" popped the head.

    That is only correct if the server acknowledges applications in the order the
    client sent them, and nothing promises that. Apply-order belongs to the client;
    acknowledgement order belongs to the server. With one application outstanding
    every possible pairing is the right one, which is why this has been shipping
    quietly. With two, an out-of-order acknowledgement pairs each capture with the
    other application's id and the popup names the wrong group -- and the group
    title is the entire point of the addon.

    The join that does exist is C_LFGList.GetApplicationInfo, whose first return is
    the search-result id the application was made against. The addon already leans
    on it at inviteaccepted (F-004); this makes it the key. Captures are filed under
    their searchResultID and "applied" resolves its appID through the same bridge to
    find one. ResolveSearchResultID is that hop split out of
    CaptureGroupInfoFromApplication, because the status arm wants the id and not a
    second round-trip for a table it already holds.

    Removing the FIFO removes something that was doing work nobody had noticed: a
    capture that never became an application used to get pushed off the front by
    later applies. Keyed by id, nothing displaces it, so the decline and cancel
    statuses now clear it explicitly. All three decline spellings are in that set.
    Blizzard sends the bare "declined" only sometimes -- a full or delisted group
    carries its reason in the status string, and those are the declines a player
    actually meets, so matching only "declined" would have left the arm nearly dead.

    Second, unrelated except that it is the same shape: autoShow was read when the
    notify timer was armed and used when it fired. It is a decision about what to do
    at fire time, so a player who switches the popup off during the delay window
    means it now. The delay beside it stays where it is and that is not an
    inconsistency -- it is the timer's own argument, and there is no later moment at
    which it could be read. The existing case pinned the old behaviour and asserted
    the popup opened anyway; it is inverted rather than added to, since both
    directions cannot be true.

    The two short status arms moved out to pairApplication and dropApplication. That
    is not tidying: inline they took the handler from CCN 15 to 18 and put the
    repo's first complexity warning on the board. The inviteaccepted merge is the
    part that has to be read as a whole, so the other two pay for it by being named.

    Written red first. Three new capture cases and the inverted notify case failed
    against the old code -- "expected Second, got First" for the two-application
    pairing, a live table where the declined capture should have been gone -- then
    541 passed, 0 failed. luacheck 0/0 in 16 files; the four-suite run is green with
    0 complexity warnings and max CCN back at 15, where it was before this change.

    docs/smoke-tests.md 5.2 said it tested the FIFO pairing and asked for three
    applications and one invite, which cannot fail for this reason. It now asks for
    two, one accepted and one declined, and says what a swap looks like. That step
    is unrun: no client is available here. The test badge moves 538 to 541 with
    docs/test-cases.md. 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 d51d04c100f54f3b5d1e3a975b203bde3d247349
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 01:26:26 2026 +0530

    M2-19: the screenshots go the same way the logo renders already did

    This file has explained since it was written that .png and .jpg are ignored
    because WoW cannot load either format, so shipping them would add megabytes per
    download for files the client physically cannot use. That reasoning was applied
    to media/logos and stopped there. media/screenshots is three tracked .png files
    in exactly those formats, 894,173 bytes, and the ignore list never named them --
    so the argument the file already makes was true of a directory it did not cover.
    WHATGROUP-R-09 is that inconsistency, and 880K here is the smallest of the four
    addons in the collection with the same gap.

    CLAUDE.md and DEPENDENCIES.md leave the package too. They are documentation for
    people working on the addon, not for people running it. README.md and LICENSE
    stay, and that is a decision rather than an oversight: players read those.

    The .claude line was already here with a comment saying dev-only, never shipped
    to players. True, but it undersold the situation and pointed the next reader at
    the wrong question. .claude is untracked -- `git ls-files .claude` returns
    nothing -- so a packager clone never carried it and removing or keeping this line
    changes no downloaded byte at all. Its comment now says "untracked; listed under
    packaging.md:28", which is the actual reason it belongs here: the standard's
    strong form MUSTs every root dot-entry present in the repo be named or justified
    whether or not git tracks it, because an enumeration goes stale silently. The
    same evidence rejected the identical .superpowers filing in three other repos
    this cycle, and it has to mean the same thing in both directions. .pkgmeta names
    itself for that reason as well; the standard's own minimum template omitted it
    until v2.39.0.

    Verified with `git ls-files` filtered through this file's own ignore: list, which
    is the only check available -- there is no packager dry-run, since no repo here
    carries .github/workflows and packaging.md:32 puts CI out of scope. It was run
    before the edit and named five shipped files at 910,932 bytes; it now reports 198
    tracked files shipping, none of them a screenshot or a dev document.

    A second agent was editing core/ and tests/ in this working tree while this
    landed, so luacheck and the harness were run against a clean archive of HEAD
    carrying only this file: 0 warnings / 0 errors, 536 passed, 0 failed, 2 skipped.
    Only .pkgmeta is in this commit. 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 a6c03ce3fd8b2b41d7b97a20873cd8affdcd74a3
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 00:48:54 2026 +0530

    M2-25: key the Master controls hook off the library, not off a copy of it

    Two literals in settings/Panel.lua that were correct on the day they were
    typed and could only ever get less correct.

    The afterGroup table keyed its reset button pair to "Master controls",
    which is a hand-typed copy of OptionsCompose.lua:50 -- a string the library
    already publishes as O.MASTER_GROUP and stamps onto every row the composer
    emits. The two agree today, which is exactly why the copy survived a
    review. What a copy cannot survive is the library renaming its own group:
    that moves the group on every composed row, and RenderTabbedSchema fires
    afterGroup PER GROUP, so a host keyed to the old spelling has a hook that
    matches nothing and simply never runs. Reset position and Reset all
    settings vanish from the tab. Nothing raises, no row goes missing, and the
    page looks perfectly ordinary -- the class of failure you find by reading
    the source a year later, if at all.

    The hook is therefore added after the constructor rather than inside it,
    under a presence check, and the check is not defensive padding. The
    library-less stub in settings/OptionsSetup.lua carries the five composer
    FUNCTIONS and none of their published DATA, deliberately, because copying
    the value sets and the wording whose nine-way drift OptionsCompose exists
    to end is the mistake the whole seam is built to refuse. So MASTER_GROUP is
    nil on that path, and [nil] = inside a table constructor raises at FILE
    LOAD -- which would take every row settings/Schema.lua had just registered
    down with it, the precise half-load that stub was written to prevent.
    Planting the unguarded version turns eighteen degraded cases red with
    "table index is nil", which is the proof that the shape is load-bearing.
    That path also composes no master rows and its tail draws nothing, so the
    right number of hooks there is zero: the absence is the degraded shape
    stated honestly, not a fallback. An `or` respelling the name would put the
    copy straight back and hide all of the above.

    The second literal was the composer's addonName, passed as "WhatGroup"
    while the file binds the first vararg at :14 like every other file in the
    repo. That vararg is the FOLDER the addon was installed into, and it is the
    only string in the process that tracks a rename or a second copy dropped in
    beside the first. It goes straight into "Enable <name>" -- a control whose
    entire job is to say which addon you are turning off. Same silent staleness
    the inline C_AddOns ladders had before core/EnvSetup.lua ended them.

    Two cases, both watched red first. The group-name one is a source guard,
    because the constant and the literal are the same interned string at
    runtime and no assertion can tell them apart; it pairs the grep with a live
    check that all six composed rows really do answer to the published
    constant. The addonName one is behavioural: tests/loader.lua grew an
    addonName option so a case can load the addon as WhatGroup_PTR and read the
    label back, which is the only way a vararg read and a literal that happens
    to match are distinguishable at all. 536 -> 538, badge and docs/test-cases.md
    moved with them.

    Two comments claimed nothing in this addon reads a published composer
    constant. One of them now does, so both say so -- and both say why
    MASTER_GROUP stays out of the stub anyway. settings-panel.md's code sample,
    ARCHITECTURE.md, common-tasks.md and module-map.md all quoted the literal
    key and now quote the constant; testing.md documents the new loader option.
    No version moved: 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 9bcc54a677025cb91094edc32db12df948c425e5
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 00:37:10 2026 +0530

    M2-21: re-ask the visibility gate when combat starts and ends

    General visibility was wired the way Master scale is wired -- read it when
    you draw, re-read it when the user changes it. That is right for a size and
    wrong for this row, because two of its four values are functions of a
    variable the user does not control and that changes without anyone touching
    the panel. Only in combat and Only out of combat were therefore correct at
    every fresh open and inert across the one transition they exist for: pull
    with the popup up and it stayed up, drop combat with the popup gated away
    and it never came back. A setting whose answer changes on its own needs an
    event, not an onChange.

    So OnEnable registers PLAYER_REGEN_DISABLED and PLAYER_REGEN_ENABLED, both
    to one OnCombatStateChanged, and ApplyFrameVisibility -- which only ever
    hid -- gained the other half. The edge is taken from the event NAME rather
    than from InCombatLockdown(), and that is not fastidiousness: the client
    fires PLAYER_REGEN_DISABLED at the start of the lockdown and the API can
    still answer false on that same frame, so a handler that asked would
    evaluate the gate against the state the player has just left and get the
    answer backwards in both directions. visibilityAllows takes an optional
    override for exactly that one caller; everyone else passes nothing and gets
    the live read, which is correct for them because they are not on an edge.

    The re-show is gated on pendingInfo. A No data popup appearing the moment
    the player pulls is worse than no popup at all, and a transition never
    builds the frame -- the lazy build is this addon's taint contract and a
    combat edge is the worst imaginable moment to trip it.

    The second half of the item is the cooldown ticker, and it is the same bug
    seen from the other side. performance-§12's declined wiring rests on one
    sentence -- the ticker cannot outlive the window that armed it -- and that
    sentence had been false since the ticker landed. applyTeleportNote armed
    before ShowFrame reached f:Show(), and OnHide, the only cancel site, fires
    on a transition; so a popup the gate declined to show left a repeating
    timer with no cancel site at all, running for the rest of the session. The
    arm now sits behind f:IsShown() and is re-run from the popup's OnShow, the
    exact mirror of the OnHide that cancels it. One seam each way, which is why
    adding a second path to the screen in the same commit did not need a second
    arm site. The register row in ARCHITECTURE.md and the sweep in
    performance.md are amended to say enforced rather than asserted; the
    exemption is not withdrawn and Perf is not wired, because only criterion
    (a)'s supporting detail slipped and (b) and (c) never depended on it.

    Eight cases, all seen red first. Five failed against today's code as
    written. The remaining three are the guards, and each was proved by
    planting the mistake it exists to catch: gating the arm on IsShown() with
    no OnShow turned "a popup that reaches the screen later still gets its
    ticker" red -- along with two cases that predate this work, which is the
    review's stated risk arriving on cue; a symmetric ApplyFrameVisibility
    without the pendingInfo clause turned "never opens a popup with nothing to
    show" red; and letting the handler reach ShowFrame turned "a transition
    with no popup built is a no-op" red. A verification that has only ever been
    green has proved nothing, and this repo now has three fewer of them.

    The mock grew a sixth modelled behaviour to make one of those honest.
    RegisterEvent recorded only that an event had been registered, so a suite
    could assert the registration exists and then call the handler by hand --
    which passes just as happily when the two are not connected to each other.
    It now records the handler name and mock.fireAddonEvent dispatches the way
    AceEvent does. That gap is widest here, where two events share a handler
    and the event name is the only thing telling it which edge it is on.

    528 to 536 cases; docs/test-cases.md and the README badge move with them.

    docs/smoke-tests.md gains section 3.8, in this commit rather than in the
    remediation bundle, because a check living only in a frozen directory in a
    thirteenth repository is runnable next cycle only by someone who knows the
    directory exists. It walks both edges in both settings, asserts nothing
    opens on a transition with no capture pending, and watches for a taint line
    at the moment a live frame is hidden from a combat-edge handler -- which no
    headless case can see. It has not been run; no client was available.

    No version moves. 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 e0c99f6105ee2ffeaef0ef25fab717a347fbbe34
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 8 00:19:50 2026 +0530

    M2-20: stop assigning the StaticPopupDialogs global

    settings/Schema.lua's EnsureResetPopup opened with

        StaticPopupDialogs = StaticPopupDialogs or {}

    directly beneath a fourteen-line comment explaining that registration is
    deferred precisely because writing to that global taints GameMenu's
    button-init closures. The guard was the very thing the paragraph above it
    forbids: an addon-author assignment to the protected table, run at the
    moment the player asks for a reset rather than at file load, but an
    assignment all the same. It also protected against nothing. Every retail
    client builds StaticPopupDialogs long before an addon file runs, so the
    `or {}` arm is unreachable and the whole line is dead.

    Deleted, keeping the indexed write below it. The comment now says out loud
    that the absence is deliberate, because the next reader who notices an
    unguarded table index is going to want to add the guard back.

    Nothing headless moves. 528 cases before and after, and the five that
    exercise this function stay green -- breaking the surviving key name turns
    them red, which is how I know the deletion sits on live ground and not on
    code the suite never reaches. That is also the limit of what the harness
    can say here: taint does not surface as a test failure, so the only real
    proof is in the client.

    Hence docs/smoke-tests.md gains section 1.4, in this commit rather than in
    the remediation bundle. The bundle is a frozen directory in a thirteenth
    repository; a check that lives only there is runnable next cycle only by
    someone who knows the directory exists. 1.4 invokes the reset popup in
    combat and out and then walks the logout path that section 1.3 has guarded
    since the original leak, which is the step that would actually catch a
    regression. It has not been run -- no client was available -- and is
    recorded as pending.

    No version moves. 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 509a36d6af0e37a9436f1db4945c5d31d982bef9
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Mon Sep 7 19:21:40 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 d82092c76ff7f9b45794033048d74b264685e7c9
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Sep 3 18:27:44 2026 +0530

    docs: repoint the citations a one-line comment edit shifted

    0458016 rewrote a comment block in core/WhatGroup.lua and grew it by one line.
    Everything below line 82 in that file moved down by one, and seven `file:line`
    citations in the doc set kept pointing at the line above the one they name.

      docs/performance.md      :185 -> :186  RegisterEvent("GROUP_ROSTER_UPDATE")
                               :186 -> :187  RegisterEvent("LFG_LIST_APPLICATION_STATUS_UPDATED")
                               :617 -> :618  the one-shot notify ScheduleTimer
      docs/slash-dispatch.md   :179 -> :180  RegisterChatCommand("wg", ...)
      docs/ARCHITECTURE.md     :514/:522/:530 -> :515/:523/:531  the join summary's
                               gold-labelled rows, cited as evidence in the
                               events-frames-taint-§8 deviation row

    Each one now lands on the statement it claims. Nothing about the addon changed;
    the numbers were right on master and only the file moved under them.

    Also in README.md's "What's new": the notification delay is set under **Chat**,
    not **Notify**. The tab strip renamed that group in 127baa1 and the rest of the
    README already says Chat -- this was the last line still sending a player to a
    section that is not on the page any more.

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

commit d82092c76ff7f9b45794033048d74b264685e7c9
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Sep 3 18:27:44 2026 +0530

    docs: repoint the citations a one-line comment edit shifted

    0458016 rewrote a comment block in core/WhatGroup.lua and grew it by one line.
    Everything below line 82 in that file moved down by one, and seven `file:line`
    citations in the doc set kept pointing at the line above the one they name.

      docs/performance.md      :185 -> :186  RegisterEvent("GROUP_ROSTER_UPDATE")
                               :186 -> :187  RegisterEvent("LFG_LIST_APPLICATION_STATUS_UPDATED")
                               :617 -> :618  the one-shot notify ScheduleTimer
      docs/slash-dispatch.md   :179 -> :180  RegisterChatCommand("wg", ...)
      docs/ARCHITECTURE.md     :514/:522/:530 -> :515/:523/:531  the join summary's
                               gold-labelled rows, cited as evidence in the
                               events-frames-taint-§8 deviation row

    Each one now lands on the statement it claims. Nothing about the addon changed;
    the numbers were right on master and only the file moved under them.

    Also in README.md's "What's new": the notification delay is set under **Chat**,
    not **Notify**. The tab strip renamed that group in 127baa1 and the rest of the
    README already says Chat -- this was the last line still sending a player to a
    section that is not on the page any more.

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

commit 90462ef8dc51d93c07b393222a1acf1e388cdf54
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Sep 3 18:03:29 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 0458016df35cddf347e156ccd673db646bce77ce
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Sep 2 21:55:11 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 08a73c6d3ccbf01f26d674f7c05d619b2cbfd220
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Sep 2 18:25:22 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 127baa1736b985bcf1b6ccfacaf27dc4c224fb7b
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Sep 2 17:56:21 2026 +0530

    feat(settings): master controls and the mandatory tab strip

    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 8122e402e22c0725ee15466de9b5e788559a7676
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 1 19:31:13 2026 +0530

    Retab the settings page, and make the popup's size a setting

    The General page was one scrolling column: ten schema rows under three
    azure section headings -- General, Notify, Frame -- with the Notify block
    alone running seven checkboxes down a single half-width column, because
    every one of them carried `solo = true`. A player looking for the popup
    toggle scrolled past six chat-line ticks to find it.

    It is a TABBED page now (options-ui-§13). RenderTabbedSchema partitions
    settings/Schema.lua by each row's `group`, in declaration order, and draws
    one tab per distinct group, so the array order IS the strip and a group's
    rows must stay contiguous. Three tabs, in the order a player meets the
    addon: General (what the addon does at all), Chat (what the chat line
    says), Popup (the window). The section headings are gone -- the strip
    carries those names -- and `section` is untouched: it is /wg list's
    grouping key, not the page's, so a row's tab is where it is EDITED and its
    path is still where it is STORED. Nothing moved paths. A profile carried
    over from the untabbed build opens with every setting where it was left,
    and /wg list prints exactly what it printed before.

    FRAME_WIDTH and FRAME_HEIGHT are settings, not file-locals. They were two
    constants at the top of modules/Frame.lua; they ship as frame.width and
    frame.height with defaults 420 and 260 -- the numbers they replaced -- so a
    popup nobody has touched is drawn exactly as it was. Sliders travel 320..700
    and 200..520, and modules/Frame.lua clamps both ON READ against the same
    bounds, because the slider cannot produce an illegal value but a hand-edited
    SavedVariable and `/wg set frame.width 4000` both can. WhatGroup:ApplyFrameSize
    resizes a live popup and REFUSES in combat -- the popup parents a
    SecureActionButtonTemplate anchored off the frame's own edges -- with every
    ShowFrame re-applying, so a refused change lands on the next open.

    Four recorded decisions are overturned, each because the tab strip removed
    the thing that justified it:

    - The vertical checklist. Every notify row was `solo` so the section read
      as a column of "include this line" ticks. The tab says that now; six
      half-empty lines became three full ones. The solo survives on the
      master, "Print to Chat", which is paired against nothing on purpose.
    - The "Show " label prefix. Under a tab called Chat, seven rows opening
      with "Show" spend their first word restating the tab. Labels only --
      notify.showInstance is still notify.showInstance.
    - Notification Delay heading the Notify section. It reads as "how long
      before the chat line", and that is half of what it does: one timer gates
      the chat line AND the popup. A row governing both surfaces belongs on
      neither of their tabs, so it sits on General with the master switch. It
      keeps its solo -- a half-width slider beside a checkbox reads as gated
      by it.
    - The Debug console checkbox pairing with "Print to Chat". That partner
      moved to the Chat tab; following it across would have put a debug
      control on the tab that decides what the chat line says. It is keyed to
      `enabled` now and lands beside Enable. Still session-only, still not a
      schema row (WG-12), still only the console window's visibility.

    The Test button's afterGroup hook is keyed to the General tab rather than
    appended to the page, which is what keeps it off the other two.

    No page banner (options-ui-§14) and none is possible: WhatGroup has no
    per-window settings and no active-window state, so there is no instance
    for a banner to name. db.global.windows stores the popup's position, which
    is geometry, not a setting.

    Docs follow the panel: settings-panel.md, schema.md, module-map.md,
    smoke-tests.md, frame.md, scope.md, common-tasks.md, ARCHITECTURE.md and
    the README's Settings-panel and FAQ sections. The stale FRAME_HEIGHT recipe
    in common-tasks.md now points at the setting and the clamp pair, and the
    file:line citations in performance.md, slash-dispatch.md, frame.md,
    module-map.md and ARCHITECTURE.md were re-resolved against today's line
    numbers. The 1.3.0 release history still names a "Notify" section; that
    prose is /wow-addon:bump-version's and is left alone.

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

commit 0b7d49dbdee86881e1694cf15f922afbb3ddda29
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Sep 1 19:30:27 2026 +0530

    Re-vendor LibKa0s v1.23.0: the tabbed options page arrives

    Options 9 -> 13 and OptionsWidgets 8 -> 12, and between them they carry
    the two surfaces this addon's settings page had no way to draw before.

    options-ui-§13 lands as O.RenderTabbedSchema and O.TabStrip: a page that
    partitions its schema by the rows' `group` field, in declaration order,
    and draws one tab per distinct group instead of stacking every section
    down one scrolling column. options-ui-§14 lands as O.PageBanner, the
    band that names which instance a page is editing. O.SetChromeHeight and
    the reworked O.RenderRows (now taking afterGroup, pairWith and an opts
    table) come with them, because a tab strip is chrome the flow engine has
    to reserve height for.

    None of it existed in the vendored v1.19.0 — not the function, not the
    descriptor field, not the layout constant — so WhatGroup's General page
    could not have been retabbed without carrying these bytes first. The
    adoption itself is the next commit; this one is only the payload.

    The kit moves with it, revision 13 -> 14: mock_base grows the frame
    members the tab-strip widgets touch, which the headless panel cases
    need in order to render a strip at all.

    The provenance line in CLAUDE.md moves in this same commit, which is
    what tests/test_vendor_sync.lua resolves the tag from — it reads the
    version out of CLAUDE.md and compares both libs/LibKa0s/ and tests/_kit/
    against that tag in the sibling checkout, so a split commit leaves the
    gate red on either side of it.

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

commit 78463d981e43d0c95b15e34cb02472bde581748a
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Aug 27 11:25:21 2026 +0530

    Say six seam files where the re-run trigger says five

    Section 9 of the smoke tests names all six LibKa0s seam files that fall back
    when their major is absent, but the closing re-run trigger still said five —
    a leftover from before the sixth seam existed. A reader who trusts the trigger
    line would leave one seam unexercised after a re-vendor, which is exactly the
    change that most needs all six checked.

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

commit 5a61ddd082af9ccb49be7dc724e00cd92a29d403
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Aug 27 11:07:47 2026 +0530

    Carry LibKa0s v1.19.0

    The library's v1.19.0 release moves Widgets to minor 8, which adds a
    ReorderList widget for settings that present an ordered list. The change
    is purely additive: nothing this addon already calls has changed shape,
    so no addon code moves with it and the six majors WhatGroup consumes
    behave exactly as they did under v1.18.1.

    The vendored payload and the CLAUDE.md provenance line move together in
    this single commit, which is what the library's release doc requires and
    what tests/test_vendor_sync.lua asserts — it reads the version out of
    CLAUDE.md and compares both libs/LibKa0s/ and tests/_kit/ against that
    tag, so a split commit would leave the gate red on either side of it.

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

commit 497c96fb5b52cabced4b210c97cd5a7de0f3eb61
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 26 19:01:27 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 f98ef41407080ced91059555c036ac42fc7b9320
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 26 17:10:40 2026 +0530

    Adopt options-ui-§12: the global reset is a profile reset, and wire the
    profile callbacks it needs

    The standard settles what the collection's global reset is: the same act
    as AceDBOptions' Reset Profile -- db:ResetProfile() on the ACTIVE profile,
    never a second walk of the schema -- behind one verbatim, addon-agnostic
    confirmation.

    Helpers.RestoreAllDefaults was closer than most: it already wiped
    db.profile before re-threading each row's default, and the comment says
    why -- a reset should yield a PRISTINE profile rather than default-valued
    known keys, dropping any value from a removed or renamed schema row. Right
    instinct, wrong mechanism. A row walk can only address ROWS, so it can
    never restore a stored array, and it was doing by hand what AceDB does
    correctly: emptying the profile IN PLACE, so anything holding db.profile
    keeps the live table, then merging the defaults back.

    AND THIS ADDON HAD NO PROFILE CALLBACKS AT ALL. Switching, copying or
    resetting a profile replaces every stored value at once and nothing
    reacted: an open settings panel kept showing the OLD profile's values
    until it was closed and reopened, and the migrations never ran on an
    incoming profile a copy could have authored at an older schema version.
    It went unnoticed because nothing here switched profiles -- until the
    global reset became one and started firing the same event.
    core/WhatGroup.lua now registers all three, and each re-runs the
    migrations and refreshes. RestoreAllDefaults therefore does NOT refresh
    itself: that would be two reconciles for one action.

    The [Reset] summary names the profile rather than a row count, because
    there is no longer a loop to count and a tally of rows written would
    understate what a profile reset empties.

    Two cases move with the behavior. The deep-copy case appended its schema
    row AFTER the db was built, which describes nothing the client can
    produce -- every real row is declared at load, and a profile reset
    restores AceDB's defaults table, built from the schema at AceDB:New time.
    It now adds the row before OnInitialize.

    Re-vendors LibKa0s v1.18.0 with it.

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

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

    feat(media): new logo art, plus the .jpg render alongside it

    Replaces the logo with the new collection-wide art and adds the .jpg render
    the repo was missing. The runtime .tga is Lanczos-downscaled from the
    2000x2000 master at 300x300 — MAIN_LOGO_SIZE in settings/Panel.lua — so the
    landing page draws it pixel-exact.

    .pkgmeta now excludes media/logos/*.png and *.jpg. WoW cannot read either
    format; only the .tga was ever loadable by the client.
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01YPd19uJ5DWdV4mKquWCSYZ

commit 24a00d6fe2d5050dbb25d9b2cdd258f17e8b22c2
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Aug 25 16:52:46 2026 +0530

    frame: drop the mark from the footer Close button

    The 90px footer button now carries only its centered label. Removes
    `decorateCloseButton` and its call; nothing else drew the catalog's
    `close` mark on it.

    standalone-windows-§33 (a wide action button SHOULD keep its label and
    gain a mark) is ratified as an accepted deviation — the row is in
    docs/ARCHITECTURE.md's deviation register, with the second-footer-button
    re-check trigger.

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

commit 5ef05efbb7fc7cba586a388f36e738c3f9228247
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Aug 25 15:35:20 2026 +0530

    docs: correct stale line citations after the LibKa0s re-vendor

    The re-vendor and the Env refactor shifted code under most of the
    file:line references the docs carry, so the citations no longer landed
    on what they name.

    - performance.md: the six event/timer rows now cite the real lines
      (WhatGroup.lua:159/:160/:591, Frame.lua:189/:520, OptionsSetup :173/:181).
    - slash-dispatch.md: RegisterChatCommand is at WhatGroup.lua:153, and
      OnSlashCommand at Slash.lua:288 — the old :294 was past EOF.
    - ARCHITECTURE.md: the events-frames-taint-8 row's join-summary rows are
      :488/:496/:504; the documentation map now names docs/revendor/, which
      exists on disk.
    - DEPENDENCIES.md: the .pkgmeta ignore list runs :6-14 since it gained
      _dev, *.bak and .claude.

    Green gate re-run after the edits: luacheck clean over 16 files,
    485/485 tests passing.

commit ce69cccf3e9af58d12301d783da9ccd18875d07f
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Aug 25 14:26:43 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 57a651e1ad6c653c687fa949af3d1a1076e6fd20
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Aug 25 12:32:40 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 d3a3a2a66dd074673724472d92907123e1803124
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Aug 25 11:05:02 2026 +0530

    automated-tests: record run 20260825-103505 — green, and a 3.19s gate

    First bundle since the collection adopted the LibKa0s fast test gate
    (test-kit revision 12). lint 0/0 over 16 files; tests 485/485, none
    skipped; perf skip — this addon ships no tests/perf.lua; complexity 0
    warnings, max CCN 15, 6377 NLOC over 906 functions.

    What changed for this repo is the wall clock: the green gate was 7.5s
    and is now 3.19s at 19% CPU. The CPU was never the problem and still
    isn't — 0.31s before, 0.27s now. The 138 processes are: the vendored-
    payload gate used to spawn one git read per blob and now batches them
    through a single `git cat-file --batch`, so popen went 147 -> 9 while
    loadfile went 24 -> 29.

    The kit's other revision-12 fix, its chunk cache, does nothing here.
    This repo calls none of Loader.load / loadAll / loadSource — every case
    needs its own _G and the kit has no mode for that, so tests/loader.lua
    keeps its own loadfile-and-cache. That code path is inert in this repo
    and is not what made the run faster.

    --jobs is left off. The suite is under the ~10s mark testing-§14 puts
    the threshold at, and Kit.run's default is jobs = 1.

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

commit a416f23583907442f8f854377a01abea5fdf32d6
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Aug 25 11:04:49 2026 +0530

    docs: resync the LibKa0s seam/payload counts and toolchain evidence

    Content-only drift pass; nothing structural moved.

    The LibKa0s seam count was still four in two places and five in two
    others. It is six — core/EnvSetup.lua and core/MediaSetup.lua joined it
    and only CLAUDE.md and ARCHITECTURE.md's subsystem table had caught up.
    Fixed in ARCHITECTURE.md's module-map diagram, testing.md's lint-scope
    paragraph, smoke-tests.md's degraded-install section and module-map.md's
    TOC-slot note.

    libs/LibKa0s/LibKa0s.xml lists thirteen <Script> entries, not nine:
    Env, Pool, Item and Widgets were missing from both file lists.
    testing.md said tests/run.lua spells that list out — tests/loader.lua
    does; run.lua holds the lifecycle factories and the suite order. The
    count is gone from the prose there rather than re-stated, since it goes
    stale on every library file that lands. Same for the two "eight minor
    constants" asides in testing.md and common-tasks.md.

    testing.md's suite list was missing test_envsetup and test_vendor_sync.

    DEPENDENCIES.md: "the suites call neither io.popen nor os.execute" has
    been wrong since the vendored-payload gate landed. The gate reads the
    sibling checkout through git (tests/_kit/vendor_sync.lua:184, :225) and
    the harness lists directories with ls -A (framework.lua:202-206); a
    missing git or ../LibKa0s is a SKIP, not a failure, and the entry now
    says so. Its setfenv/loadstring evidence lines and its five
    docs/testing.md line citations had all shifted, and the lizard note
    pointed at a testing.md section that no longer exists.

    Gate green after: tests 485/485, luacheck 0/0 over 16 files,
    docs/test-cases.md byte-identical to a fresh --list.

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

commit 28c09168d46dfea6a857ce87ca0895f8e71a8de4
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Aug 25 04:10:33 2026 +0530

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

    Both of this addon's copies of the metadata ladder were INLINE at the call site
    — settings/Slash.lua's version() and settings/Panel.lua's addNotesLine — so an
    audit of core/Compat.lua would have reported WhatGroup as having no copy at all.
    core/EnvSetup.lua is now the one place either is read from.

    Compat keeps what is genuinely this addon's: the version-variant spell and LFG
    shims. No behaviour change; the seam's fallbacks repeat the ladder both copies
    ran, so an install missing LibKa0s still reads its own TOC.

    Two members only — this addon stamps neither a zone nor a map id. The version
    fallback is WhatGroup.VERSION rather than the NS.version most of the collection
    uses, read at call time because core/WhatGroup.lua loads after the seam.

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

commit 8edf4f4582c6d2bb94c795a1b822097b5d51a073
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Tue Aug 25 03:50:35 2026 +0530

    chore(libs): re-vendor LibKa0s v1.15.0

    Adds LibKa0s-Env-1.0, LibKa0s-Pool-1.0 and LibKa0s-Item-1.0 to the payload and
    takes Widgets to minor 6 (CopyWindow) and DebugLog to minor 11 (the console
    holds 1500 lines, not 500). Nothing in this addon adopts the new majors yet —
    this commit is the copy and the provenance line.

    The one behaviour change is the console history depth, which is the library's
    and arrives with the copy. The prose that quoted the old cap moves with it:
    docs/debug.md, and also docs/ARCHITECTURE.md, docs/smoke-tests.md and a
    settings/Schema.lua comment, which named 500 too.

    tests/_kit goes to revision 12 in the same commit. That is NOT a change this
    release makes — LibKa0s' testkit is untouched by v1.15.0. Revision 12 shipped at
    v1.14.0 ("testkit revision 12: the green gate stops waiting on the disk") and
    this addon never took it. The kit moves now because it must: vendor_sync.lua
    registers libs/LibKa0s and tests/_kit as two cases against the single tag
    bundledVersion() greps out of CLAUDE.md, so one provenance line governs both
    payloads and there is no green state in which the line says v1.15.0 and the kit
    is still 11. Revision 12 is backward compatible (loader chunk cache, opt-in
    --jobs/--shard defaulting to 1), so no runner change was needed.

    diff -r against the source repo is empty for both payloads.

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

commit 013ed010002318d5050555306d8ab41e318e53b0
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Mon Aug 24 21:05:35 2026 +0530

    Teach the popup the Midnight season 2 dungeon teleports

    Season 2 rotated the M+ pool and the table only knew season 1, so seven of
    the eight dungeons showed no teleport at all: the popup skipped the row and
    the join summary dropped its Teleport line for a player who owns the spell.

    Five are new Midnight dungeons; Kings' Rest and Temple of Sethraliss are BfA
    dungeons that got their first teleport with the season, so both are single
    IDs rather than candidate lists. Ruby Life Pools is the eighth and keeps its
    Dragonflight spell.

    Every ID and name comes from a spellbook dump on a character that owns them,
    not from a wiki lookup — the failure mode for a near-miss ID is a row that
    renders desaturated rather than one that errors. The eight rows are pinned in
    tests/test_labels.lua for the same reason the season 1 four are.

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

commit 5e636159ba2dee9158d0c647c951604b2f62e97b
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Mon Aug 24 02:26:38 2026 +0530

    Take the console's font and its marks from the shared library payload

    The debug console's title bar drew the words Copy and Clear beside a
    multiplication sign, and the monospace face it renders in was this addon's own
    copy of JetBrains Mono under media/fonts/. Both are LibKa0s' now
    (LibKa0s-Media-1.0, v1.10.x), so this is the wiring.

    SIX ADDONS SHIPPED SIX COPIES OF ONE FONT. Two copies of a face is two licenses
    to track and two provenance stories, and the collection stops looking like one
    author's work the first time one copy is regenerated and the rest are not. The
    face moves into libs/LibKa0s/media/fonts/ with the icon catalog beside it, and
    media/fonts/ is deleted. media/logos/ and media/screenshots/ stay: the brand TGA
    the settings landing page draws is genuinely this addon's, and .pkgmeta ships
    media/ to players.

    WG-20 CLOSES WITH IT. The deviation existed to justify vendoring a non-Blizzard
    font privately; there is nothing private left to justify, so it collapses into
    the library-stack media rule. WG-21, the logo, is untouched.

    TWO SEAMS NOW SAY WHO IS ASKING. A texture path is absolute from
    Interface\AddOns\ and LibKa0s is vendored, so it cannot work out which folder it
    was copied into; the addon folder name is the answer and only the addon has it.
    core/CoreSetup.lua wraps MakeCloseButton to supply it -- one wrapper, so every
    close control this addon ever builds gets the same mark without any call site
    remembering -- and core/DebugLogSetup.lua passes addonName in the console
    descriptor, which turns Copy, Clear and the multiplication sign into the shared
    copy, clear and close marks on the console and on its copy window at once.

    addonName is passed EXPLICITLY beside name rather than inferred from it. They are
    the same string here and different questions everywhere: name seeds
    WhatGroupDebugWindow and its siblings, and a host where the two diverge would
    hand the library a path into nowhere -- which draws nothing and raises nothing.

    core/MediaSetup.lua is new and its TOC slot is load-bearing rather than
    conventional: core/WhatGroup.lua resolves NS.FONT_MONO from NS.MediaFont at load
    and core/DebugLogSetup.lua hands that value to a descriptor the library validates
    as a string, so it sits immediately after core/CoreSetup.lua and before both. The
    resolution ends in `or _G.STANDARD_TEXT_FONT` on purpose: SetFont accepts a path
    to a file that is not there, fails to load it, and the text simply does not draw,
    so a degraded install has to land on a face the client definitely has. ARIALN is
    still the debug-logging-§2 fetch-failure rung; it is now the third, not the
    second. The addon's own LSM:Register call is gone, replaced by the library's
    RegisterLSM at file load -- one key, one set of bytes, every Ka0s addon agreeing
    instead of colliding.

    The popup's footer Close button takes a mark BESIDE its label, not instead of it.
    It is a 90px templated button in a footer, not a small square target in a title
    strip, and the construction went into a named helper rather than inline: adding
    an art path to an already-long window builder is what took a LibKa0s function one
    over the release CCN ceiling. No title-bar close was added to the popup -- that
    would be new chrome rather than a re-skin, and this addon's window has always
    closed from its footer and from Esc.

    Bundles LibKa0s v1.10.2: libs/LibKa0s/ and tests/_kit/ re-vendored from the tag,
    provenance line moved with them, kit revision 11. v1.10.2 rather than the v1.10.1
    the adoption brief named, because this addon vendors PerfPanel and v1.10.2 is the
    release that stops its close button being built without the addon name -- the
    same defect this commit fixes one window over.

    THE TESTS PIN THE ARGUMENT, NOT THE APPEARANCE, because the appearance is exactly
    what no out-of-game gate can see: the factory receives no name, builds no texture
    path, and draws a perfectly good button. tests/test_mediasetup.lua is new and
    cross-checks every icon this addon draws against both the library's catalog and
    the files in the vendored copy; tests/test_libka0s.lua gains the two cases that
    assert the folder name reaches the close-button factory as its third argument and
    that the descriptor carries addonName. docs/smoke-tests.md gains section 12 for
    the half only a player can check, including what each regression looks like: two
    words and a multiplication sign on the console 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 9783d25c3fe9d5518f33b690c38a2c9775edb179
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Aug 7 14:00:22 2026 +0530

    docs: name the in-game capture store perf-analysis, not perf-runs

    The Ka0s WoW Addon Standard retires the flat docs/perf-runs/ store and
    replaces it with docs/perf-analysis/, where each capture is a frozen
    dated bundle holding the record and its reading together.

    This addon ships neither, and still ships neither. Its performance-§12
    exemption is unchanged: nothing is wired, no capture is ever produced
    here, and no docs/perf-analysis/ directory is created by this commit.
    What changed is only the name the docs use when they say what is absent,
    so a reader chasing the phrase in the standard finds the same words
    here. The exemption prose, its criteria and its re-check triggers are
    untouched.

    Three live docs carried the old name: the ARCHITECTURE.md conditional-doc
    row and both performance-§12 deviation rows, performance.md's list of
    what is not wired, and automated-tests/README.md's pointer to where
    in-game captures would live.

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

commit e617aa18d63e446283749402826c829d62452b01
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Aug 7 12:22:28 2026 +0530

    automated-tests: record run 20260807-121935 — the watch list is empty

    First recorded run since b1511f6 split ConfigureTeleportButton, so the
    record now agrees with the code: max CCN 20 -> 15, CCN warnings 1 -> 0.
    That entry was the last thing blocking this addon's release gate, which
    refuses zero functions above CCN 15.

    lint 0/0 over 14 files; tests 462/462, none skipped; perf skip (ships no
    tests/perf.lua); complexity 6114 NLOC / 870 funcs, avg NLOC 6.4, avg CCN
    1.7 — both averages unmoved, so the addon did not get denser, it got the
    same code across four more, smaller functions.

    test-cases.md came out byte-identical to the previous run's despite a
    real change to the file the teleport cases exercise. That is the point
    worth recording: the 20 existing cases verified the refactor with none
    added, renamed or adjusted to fit it.

    ANALYSIS.md also keeps the diagnosis, because the obvious reading was
    wrong — lizard scores Lua closures as separate functions, so the nested
    handlers contributed nothing to the parent's 20; it was 12 genuine
    top-level branches. Anyone reaching for "it's just or-defaulting" on a
    future entry should check the token counts first.

    New ceiling is core/WhatGroup.lua:634-697 at CCN 15 — passing by one
    point, and with no seam of the kind that made this split easy.

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

commit b1511f67f50b410f0e067af927d047565109edec
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Aug 7 12:16:36 2026 +0530

    refactor(frame): split ConfigureTeleportButton, CCN 20 -> 6

    At CCN 20 it was the collection's only complexity warning and would have
    refused the next tag — /wow-addon:bump-version gates a release on zero
    functions above CCN 15.

    The score was not an artifact of dense defaulting. The body carried 12
    real branches doing five separable jobs, so this extracts four of them
    as file-scope locals alongside the existing forward declarations, which
    is what lets them reach `f`, `fields` and the recursive
    ConfigureTeleportButton as upvalues:

      deferTeleportUntilCombatEnds  CCN 2   the lockdown stash + regen retry
      resolveTeleportState          CCN 11  spell, known, remaining, ready
      applyTeleportNote             CCN 3   the three-state note + ticker
      applyTeleportAction           CCN 4   macro-vs-cleared, tooltip, trace
      ConfigureTeleportButton       CCN 6   the state machine it always was

    Pure extraction — no behaviour change, and the 20 teleport cases in
    tests/test_frame.lua cover every branch: combat defer and rebuild,
    unlearned, on-cooldown, ready, swipe arming, the ticker counting down
    and re-arming the cast at zero, stale-macro clearing. 462/462 green
    after each step; lint 0/0; repo-wide complexity warnings now 0.

    The load-bearing comments moved with their code rather than being
    summarised — why stopCooldownTicker() runs first, why :Disable() is the
    wrong tool on a protected frame, why the note's three-state order
    matters, why the swipe gets the raw pair.

    Re-anchors the six docs citations of modules/Frame.lua:327 (the ticker)
    to :146. The two in docs/reviews/2026-08-03/ are frozen evidence and
    stay as recorded.

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

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

    automated-tests: record run 20260807-114405 — green

    Four-suite run through the vendored kit (LibKa0s v1.8.2, test-kit
    revision 10). lint 0 warnings / 0 errors over 14 files; tests
    462 passed, 0 failed, 0 skipped; perf skipped — no tests/perf.lua;
    complexity 1 warning — ConfigureTeleportButton at CCN 20 (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 4a32cbef035ace557250225ada862b3b3ebf9486
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Aug 7 11:37:57 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 17d0ad7c7b156fe4496ba16b5bd7548e198ebe9b
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Aug 7 11:05:24 2026 +0530

    vendor: re-vendor LibKa0s v1.8.2 — kit revision 10, CRLF-aware runner

    Both payloads move together, from the tag: libs/LibKa0s/ and tests/_kit/ are
    whole-folder copies of LibKa0s at v1.8.2, and the CLAUDE.md provenance line
    moves in this same commit — tests/test_vendor_sync.lua reads that line and
    compares both folders against the tag, so a half-move fails the gate.

    Kit revision 10 writes each bundle file to whatever .gitattributes declares for
    it (git check-attr text eol, per path), instead of down a plain redirect that
    bypassed git's filters and left LF stragglers in a CRLF-pinned repo. Bundle
    20260807-110421 is the first written by the fixed runner: every file lands CRLF.

    Battery: lint pass (0/0, 14 files), tests pass (462/462, vendor_sync included),
    perf skip (no tests/perf.lua), complexity pass (1 warning, max CCN 20,
    recorded, non-gating). Verdict green.

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

commit 8d05986f4e9b73af8fc5e276c4279338476d0894
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Aug 7 09:54:10 2026 +0530

    tests: name the debuglog scrollbar case with the canonical debug-logging-§11

    The case cited a bare "§11", which does not say which standards file the
    section belongs to and does not match the filename-§N form every other
    citation in the suite uses. Regenerated docs/test-cases.md from
    `lua tests/run.lua --list` so the inventory tracks the rename.

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

commit e52cc07b3cac4adec628fb968945b5d60b6f8e97
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Aug 7 03:18:41 2026 +0530

    docs: point the deviation rule at the register; resolve stale citations

    CLAUDE.md's "Standards compliance (read first)" still told an agent to record
    an accepted deviation as an in-code SHOULD-justification comment. documentation-§6
    now names one home: a row in docs/ARCHITECTURE.md -> ## Documented deviations,
    shaped | Rule | What differs | Why | Decided | Re-check trigger |. The repo
    already keeps that register, so this is text catching up to practice; the
    stop-and-flag directive and the two-way classification are unchanged.

    Also resolves citations that no longer resolve, against standard v2.28.0:

      .luacheckrc:1              (§14) -> (lint); lint.md carries no numbered
                                 subsections, so it takes a bare filename
      docs/debug.md:5            `debug-logging §` -> `debug-logging`
      docs/module-map.md:16      (debug-logging §) -> (debug-logging)
      docs/ARCHITECTURE.md:180   (§2) and §2's -> debug-logging-§2
      docs/debug.md:76,291       §11 -> debug-logging-§11

    The .luacheckrc edit is comment-only; no line that runs was touched.

commit 0446f45c8a55d18cd0c9385ffa0012cb6fd468b7
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Aug 7 02:31:05 2026 +0530

    automated-tests: record 20260807-022625; CCN 20 newly warned

    The recorded run was three days and five commits stale, and the tree it
    described no longer exists: 422 cases against today's 462, 5573 NLOC
    against 6095. This is the full four-suite run over HEAD (4e99ba5, clean),
    with the ANALYSIS.md and the four RESULTS.md standing sections written
    from this bundle's manifest.json and suite artifacts only.

    Both gating suites are green — luacheck 0/0 over 14 files, 462/462 cases
    with zero skips — so nothing here blocks a commit. The finding is on the
    non-gating side: ConfigureTeleportButton (modules/Frame.lua:240-396) has
    newly crossed CCN 15, at 20, up from 12 last run. The WG-31 cooldown
    states (c7d8e2d) roughly doubled it on every axis. It is genuine control
    flow rather than the usual Lua or-defaulting artefact, so it is
    dispositioned "Peel next", not Accepted — it is the entry's first
    appearance, and it would fail the release gate as it stands.

    perf remains a skip with zero scenarios, cited to the ratified
    performance-§12 (re-check fired) row rather than left to read as an
    unmeasured addon. The RESULTS.md lint section's excluded-tree figure is
    also refreshed: the tests/ tree is now 7212 lines across 22 files, up
    from the 6098/20 last stated.

    The authorised comment-citation sweep applied nothing. Every path and
    Symbol.Member citation in the addon's own source resolves; the three
    that do not are deliberate history (core/DebugLog.lua in two past-tense
    sentences describing the move to LibKa0s, and a cross-repo provenance
    note), which are correct as written and were left alone.

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

commit 4e99ba5b015c05c0c67d84316fe65426e13517f1
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Fri Aug 7 02:11:45 2026 +0530

    docs: correct the post-ticker perf claims and stale file:line citations

    The teleport cooldown countdown ticker landed on 2026-08-06 and the deviation
    table was updated for it, but three places still described the addon as having
    no repeating timer at all: ARCHITECTURE's Event Subscriptions prose (which also
    still called the countdown text "static"), performance.md's opening claim of the
    no-combat-path exemption and its re-check section, and module-map.md's reason
    for declining LibKa0s-Perf. All now say what the code does.

    Line-number citations that drifted:
    - modules/Frame.lua:321 -> :327 (the ScheduleRepeatingTimer) in ARCHITECTURE,
      performance.md and frame.md
    - modules/Frame.lua:447 -> :453 (the combat wait frame)
    - core/WhatGroup.lua:563 -> :577 (the one-shot notify ScheduleTimer)
    - core/WhatGroup.lua:460/:468/:476 -> :474/:482/:490 (the gold-labelled join
      summary rows cited by the events-frames-taint-8 deviation row)
    - libs/LibKa0s/Slash.lua:505 -> :586 (Sl:OnSlash)

    DEPENDENCIES.md claimed the repo contains no .sh at all; tests/_kit/run-automated-tests.sh
    is right there and is documented in docs/testing.md. Says so now, with bash
    named as what it needs.

    Docs only - no code, no version change.

commit 26eb52bf6dda66de8727e278245b4a708ae8dd0c
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Aug 6 21:21:41 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 5278c0ac4bd2827c02b565387ab3f16087341cb5
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Aug 6 18:38:27 2026 +0530

    Move the LibKa0s provenance line to CLAUDE.md; re-vendor testkit r9 (LibKa0s v1.8.1)

    The provenance line — "Bundles [LibKa0s](…) vX.Y.Z (MIT)." — is the input the
    vendored-payload gate reads to decide which LibKa0s tag libs/LibKa0s/ and
    tests/_kit/ are supposed to match. It used to live in README.md. README.md is
    the player-facing page, and it is being reduced to what a player actually needs,
    so a gate reading its input out of a file whose whole direction of travel is to
    stop mentioning that input was on a countdown. The line now lives in the root
    CLAUDE.md, beside the rest of the build facts a maintainer or an agent needs.

    testkit goes from revision 8 to revision 9 (LibKa0s v1.8.1), whose only change
    is that vendor_sync.lua reads CLAUDE.md instead of README.md, with no fallback:
    a repo that has not moved its line reads as carrying no provenance line at all
    and fails, naming the file it looked in. Re-vendor and line move land in the
    same commit because the gate compares file sets and reads the line as its input;
    they cannot be split. libs/LibKa0s/ is untouched — no shipped library file
    changed in v1.8.1.

    README.md also loses the bundled-library paragraph (the "everything ships
    inside it" prose plus the Ace3 / LibSharedMedia / LibKa0s inventory). Nothing
    in it was an external credit, so no Credits section survives it. The Standard
    badge is unlinked.

    docs/test-cases.md regenerated: revision 9 renames one vendor_sync case, counts
    unchanged at 462/462, so the README tests badge does not move.

    Prose swept where it still pointed at README.md for provenance: docs/testing.md,
    docs/common-tasks.md, docs/module-map.md and the header comment on
    tests/test_vendor_sync.lua. Frozen dated bundles under docs/audits/ and
    docs/reviews/ left alone.

    Green: 462 passed, 0 failed, 0 skipped, 462 total; luacheck 0 warnings / 0 errors.

commit 7a6d14794e1b4ec089e05ac7860f0156e67ef1db
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Aug 6 17:23:22 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 6f3a37a79ea13bacd2e7d5fc6d6090ec1b890088
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Aug 6 16:00:57 2026 +0530

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

    This repo carried four of the collection's five naming collisions on its own,
    which is what made the drift visible when the eight docs/ trees were read side
    by side. Every one of these names is defensible in isolation and wrong in a
    collection where the same content has a different name in seven other repos.

    Renamed (git mv, history preserved):
      capture-pipeline.md -> data-flow.md
      settings-system.md  -> settings-panel.md
      wow-quirks.md       -> midnight-quirks.md
      debug-console.md    -> debug.md
      file-index.md       -> module-map.md   (this repo had no module-map.md; the
                                              file index WAS the module map here)

    Created: schema.md — the last missing Tier 1 doc. It records the deliberate
    two-site split that reconciles savedvariables-§2 with architecture-§5: values
    in defaults/Profile.lua as NS.C, structure in settings/Schema.lua with each
    row's default REFERENCING C.<path> rather than restating the literal. It also
    states what is deliberately not persisted — debug is session-only, and the
    General panel's Debug console checkbox toggles the window's visibility alone,
    never the logging flag and never db.profile, which is what keeps the WG-12
    invariant true.

    Retired: file-index.md (folded into the module map it duplicated).

    ARCHITECTURE.md gains `## Documentation map`. message-bus.md is recorded as
    "Not applicable" for the honest reason: this addon defines no cross-module
    messages at all, being a single feature module.

    Verified: 462 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 c7d8e2d550828bfcceb66044953cc06e9cc54930
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Aug 6 14:57:38 2026 +0530

    feat(frame,notify): the teleport button says why it cannot be cast

    A greyed-out teleport had one explanation in the popup and one in chat, and
    the popup's was silence: the icon desaturated and nothing said why. Two
    reasons produce that state and they call for opposite responses — learn the
    spell, or wait — so the popup now names which, in a note beside the button,
    and the chat line gains the cooldown tag alongside its existing one.

    Cooldown is read through two new Compat shims (core/Compat.lua:74) that
    normalize retail's C_Spell.GetSpellCooldown table against the legacy
    multi-return. Two judgement calls are encoded there. Anything at or under the
    1.5s GCD floors to zero, because otherwise casting anything at all makes an
    eight-hour teleport blink "on cooldown". And isEnabled == false means "do not
    draw a cooldown" — the spell is mid-cast — so it reads as ready rather than
    greying the button during every cast.

    The popup's three states, in modules/Frame.lua. Ready: full alpha, macro
    armed. Recharging: desaturated, a CooldownFrameTemplate swipe over the icon,
    and "On cooldown - 7h 58m 12s" counting down beside it. Not learned:
    desaturated and "Teleport spell not learned". Not-learned is tested FIRST,
    because an unlearned spell can still report a cooldown and naming the cooldown
    buries the reason the icon is grey.

    Clearing the secure attributes IS the disable — the button still takes the
    click, the secure handler finds no action, nothing casts. :Disable() is the
    obvious alternative and the wrong tool on a protected frame. The tooltip
    survives a cooldown but not an unlearned spell: on cooldown it is where the
    spell's own text and exact timing live, and a player who owns the teleport is
    entitled to it.

    The chat tag carries no time remaining, deliberately. That line is printed
    once into scrollback with no way to refresh itself, so a figure there would be
    wrong within a second and stay wrong.

    STANDARDS: the 1-second countdown timer ends performance-§12's no-combat-path
    exemption, whose re-check trigger names "the first repeating ticker" in those
    words. Recorded as a ratified deviation row in docs/ARCHITECTURE.md rather
    than resolved quietly, with the sweep in docs/performance.md regenerated to
    name the timer and its per-tick cost. Only criterion (a) broke; (b) and (c)
    stand, so the wiring is still declined. What makes it defensible is that the
    timer cannot outlive the popup: one handle, replaced not stacked, cancelled
    from OnHide, from the top of every ConfigureTeleportButton, and by the tick
    that reaches zero. An amendment is filed upstream at WowAddonStandards
    f3354b2 proposing that §12 exempt a window-bounded ticker; if it lands, the
    row retires.

    Also: /wg test moves from The Stonevault to Windrunner Spire (mapID 2805) so
    the Midnight teleport path is what the manual affordance exercises. Its
    activityID becomes synthetic — the old 2516 is itself a TeleportSpells key, so
    the fixture would have resolved a teleport through the activityID fallback
    with the mapID path broken.

    Written test-first throughout: 29 new cases, each failing for its own reason
    before the code existed. The mock gains ScheduleRepeatingTimer as a distinct
    object from a one-shot, so a suite can prove a ticker was cancelled rather
    than assume it.

    Verification: 462 passed, 0 failed; luacheck 0 warnings / 0 errors in 14
    files; vendor gate clean. No version bump.

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

commit c7d8e2d550828bfcceb66044953cc06e9cc54930
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Aug 6 14:57:38 2026 +0530

    feat(frame,notify): the teleport button says why it cannot be cast

    A greyed-out teleport had one explanation in the popup and one in chat, and
    the popup's was silence: the icon desaturated and nothing said why. Two
    reasons produce that state and they call for opposite responses — learn the
    spell, or wait — so the popup now names which, in a note beside the button,
    and the chat line gains the cooldown tag alongside its existing one.

    Cooldown is read through two new Compat shims (core/Compat.lua:74) that
    normalize retail's C_Spell.GetSpellCooldown table against the legacy
    multi-return. Two judgement calls are encoded there. Anything at or under the
    1.5s GCD floors to zero, because otherwise casting anything at all makes an
    eight-hour teleport blink "on cooldown". And isEnabled == false means "do not
    draw a cooldown" — the spell is mid-cast — so it reads as ready rather than
    greying the button during every cast.

    The popup's three states, in modules/Frame.lua. Ready: full alpha, macro
    armed. Recharging: desaturated, a CooldownFrameTemplate swipe over the icon,
    and "On cooldown - 7h 58m 12s" counting down beside it. Not learned:
    desaturated and "Teleport spell not learned". Not-learned is tested FIRST,
    because an unlearned spell can still report a cooldown and naming the cooldown
    buries the reason the icon is grey.

    Clearing the secure attributes IS the disable — the button still takes the
    click, the secure handler finds no action, nothing casts. :Disable() is the
    obvious alternative and the wrong tool on a protected frame. The tooltip
    survives a cooldown but not an unlearned spell: on cooldown it is where the
    spell's own text and exact timing live, and a player who owns the teleport is
    entitled to it.

    The chat tag carries no time remaining, deliberately. That line is printed
    once into scrollback with no way to refresh itself, so a figure there would be
    wrong within a second and stay wrong.

    STANDARDS: the 1-second countdown timer ends performance-§12's no-combat-path
    exemption, whose re-check trigger names "the first repeating ticker" in those
    words. Recorded as a ratified deviation row in docs/ARCHITECTURE.md rather
    than resolved quietly, with the sweep in docs/performance.md regenerated to
    name the timer and its per-tick cost. Only criterion (a) broke; (b) and (c)
    stand, so the wiring is still declined. What makes it defensible is that the
    timer cannot outlive the popup: one handle, replaced not stacked, cancelled
    from OnHide, from the top of every ConfigureTeleportButton, and by the tick
    that reaches zero. An amendment is filed upstream at WowAddonStandards
    f3354b2 proposing that §12 exempt a window-bounded ticker; if it lands, the
    row retires.

    Also: /wg test moves from The Stonevault to Windrunner Spire (mapID 2805) so
    the Midnight teleport path is what the manual affordance exercises. Its
    activityID becomes synthetic — the old 2516 is itself a TeleportSpells key, so
    the fixture would have resolved a teleport through the activityID fallback
    with the mapID path broken.

    Written test-first throughout: 29 new cases, each failing for its own reason
    before the code existed. The mock gains ScheduleRepeatingTimer as a distinct
    object from a one-shot, so a suite can prove a ticker was cancelled rather
    than assume it.

    Verification: 462 passed, 0 failed; luacheck 0 warnings / 0 errors in 14
    files; vendor gate clean. No version bump.

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

commit cbb6661d0c04683028c44563bea72d82ef74cb58
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Thu Aug 6 13:12:38 2026 +0530

    fix(teleports): Nexus-Point Xenas is 1254563 — 1254553 was a typo

    A reporter joined a +10 Xenas group and got a desaturated teleport icon
    and a chat line reading "Teleport: [Hero's Path] (not learned)", while a
    second addon offered them a working teleport for the same dungeon.

    1254553 is a real spell — named "Hero's Path" — so nothing errored.
    GetTeleportSpell resolved known=false and the frame did exactly what it
    should with an ID nobody holds. The Keystone Hero reward is 1254563,
    "Path of the Fractured Core"; e30c1f6 transposed a digit while claiming
    tooltip verification.

    Siege of Boralus had the same defect from the same cause: [1822] carried
    only 464256, a wiki ID, while players hold 445418. Both resolve to "Path
    of the Besieged Harbor", so this is a re-issue like The MOTHERLODE!!, not
    a wrong ID — the row becomes { 445418, 464256 } with the spellbook-
    confirmed spell leading, which is also the fallback when a player knows
    neither.

    Both bugs came from looking spells up by dungeon name. That cannot work:
    a teleport's name never contains its dungeon's, so the search returns
    something adjacent rather than nothing. So every row now carries its
    spell's real name, read back from C_Spell.GetSpellInfo in-game across all
    78 IDs — the dungeon/name pair is a standing cross-check, and a name that
    does not fit its dungeon is a wrong ID. All 75 rows check out. The header
    explains the trap and the silent failure shape; docs/common-tasks.md
    promotes the spellbook sweep over the wiki, which produced both errors.

    Also: Dungeons/Raids subsections under each expansion, the "(raid)"
    suffix dropped as redundant, Tazavesh shortened, and the Midnight-only
    warning block moved to the file header where it always belonged.

    Two regression tests pin the four Midnight rows to spellbook IDs and
    Boralus' candidate ordering.

    Verification: 435 passed, 0 failed; luacheck 0 warnings / 0 errors in 14
    files. No version bump.

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

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

    docs(frame,test-cases): the teleport button carries both click edges

    e233cf3 restored `RegisterForClicks("AnyUp", "AnyDown")` in
    modules/Frame.lua but left two docs describing the [M4-24] state that
    commit reverted.

    1. docs/frame.md:66 said the button was registered for `AnyDown` — "one
       edge, deliberately" — and repeated the premise e233cf3 disproved, that
       the pair casts twice per press. It now says what the code does and why:
       `AnyUp` is the edge that actually executes the macro on a bare
       SecureActionButtonTemplate, `AnyDown` alone casts nothing with no Lua
       error to notice, and the PreClick `down` gate is what keeps two edges to
       one debug line.

    2. docs/test-cases.md regenerated — the renamed frame case still read
       "registers exactly one click edge". Generated file, so this is
       `lua tests/run.lua --list > docs/test-cases.md`, not a hand-edit.

    Totals unchanged at 433, so the README badge already agrees.

    Verification: 433 passed, 0 failed; luacheck 0 warnings / 0 errors in 14
    files; lizard 0 warnings, no CCN > 15; vendor gate clean on all four
    readings against ../LibKa0s (LibKa0s and testkit, content and bytes).

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

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

    docs(frame,test-cases): the teleport button carries both click edges

    e233cf3 restored `RegisterForClicks("AnyUp", "AnyDown")` in
    modules/Frame.lua but left two docs describing the [M4-24] state that
    commit reverted.

    1. docs/frame.md:66 said the button was registered for `AnyDown` — "one
       edge, deliberately" — and repeated the premise e233cf3 disproved, that
       the pair casts twice per press. It now says what the code does and why:
       `AnyUp` is the edge that actually executes the macro on a bare
       SecureActionButtonTemplate, `AnyDown` alone casts nothing with no Lua
       error to notice, and the PreClick `down` gate is what keeps two edges to
       one debug line.

    2. docs/test-cases.md regenerated — the renamed frame case still read
       "registers exactly one click edge". Generated file, so this is
       `lua tests/run.lua --list > docs/test-cases.md`, not a hand-edit.

    Totals unchanged at 433, so the README badge already agrees.

    Verification: 433 passed, 0 failed; luacheck 0 warnings / 0 errors in 14
    files; lizard 0 warnings, no CCN > 15; vendor gate clean on all four
    readings against ../LibKa0s (LibKa0s and testkit, content and bytes).

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

commit 8335b73c91f89ba15ab54257c70d8b0258603226
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 22:17:55 2026 +0530

    data(teleports): restore Halls of Infusion at its real mapID 2527

    Completes b421c6d, which parked the row rather than guess its key after
    luacheck caught it colliding with Uldaman on 2451.

      [2527] = 393283,   -- Halls of Infusion

    mapID confirmed by the reporter. Both Dragonflight dungeons now resolve to
    their own teleport instead of one shadowing the other, and the Dragonflight
    block is back to sorted key order.

    Verification: 433 passed, 0 failed; luacheck 0 warnings / 0 errors in 14
    files; 2451 -> 393222, 2527 -> 393283. Table 74 -> 75 rows.

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

commit b421c6d76d70065b22bc81c1138c81a071878eff
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 22:17:23 2026 +0530

    fix(teleports): Uldaman is 2451 — the Halls of Infusion row was misfiled

    Three changes, all from in-client tooltips supplied by the reporter.

    1. Uldaman: Legacy of Tyr enabled at its real mapID.

         [2451] = 393222,   -- Path of the Watcher's Legacy

       It had sat commented out awaiting a mapID since the file was written.

    2. Halls of Infusion parked, because it was occupying 2451.

       luacheck caught the collision the moment Uldaman went in (W314, value
       assigned to field 2451 overwritten before use). Since 2451 is Uldaman,
       the pre-existing Halls of Infusion row was misfiled under it -- so
       Uldaman groups were offered Halls' teleport (a wrong-spell cast, the
       same failure class as the Nexus-Point Xenas row corrected in e30c1f6)
       and Halls groups got no button at all.

       Parked rather than guessed. The spell (393283) is kept in the comment;
       only the key is unknown. Net effect is strictly an improvement: Uldaman
       becomes correct, Halls stays absent, which it effectively already was.
       It needs its real mapID to come back.

    3. Dawn of the Infinite's "(validate mapID)" marker removed -- 424197
       confirmed as Path of Twisted Time. Both Dragonflight rows now carry
       their spell name, matching the Midnight block's convention, because the
       "Path of <flavour name>" names do not contain the dungeon name and are
       not recoverable from the row without them.

    Verification: 433 passed, 0 failed; luacheck 0 warnings / 0 errors in 14
    files (the W314 collision is gone); 2451 -> 393222, 2579 -> 424197,
    2915 -> 1254553. Table holds at 74 rows: Uldaman in, Halls out.

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

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

    fix(teleports): Nexus-Point Xenas is 1254553, not 1255391

    Corrects the row added in 55f5567. I picked the wrong spell.

    1255391 is "Teleport: Nexus-Point Xenas" -- a different spell that shares the
    destination. The Keystone Hero reward the player actually learns is
    "Path of the Fractured Core", spell 1254553, confirmed from the in-game
    tooltip. With the wrong ID the button would have rendered desaturated and
    its /cast would have named a spell the player does not have: the same shape
    of silent failure this file's other rows exist to avoid.

    Two claims in 55f5567 were also wrong and are removed:

    - "Midnight renames these to Teleport: <Dungeon>". It does not. The Keystone
      Hero teleports keep the "Path of <flavour name>" convention throughout.
    - The implication that the spell can be found by searching the dungeon name.
      It cannot: "Path of the Fractured Core" does not contain "Nexus-Point
      Xenas" anywhere, which is exactly why a name-based web search found the
      wrong spell and an in-game tooltip found the right one.

    The comment above the Midnight block now says both things outright, and names
    1255391 explicitly as the decoy, so the next person does not repeat this.
    Each Midnight row gains its spell name in a trailing comment.

    Verified against in-game tooltips supplied by the reporter -- 7 of the 8
    Midnight S1 pool rows already matched (Windrunner Spire 1254400, Magisters'
    Terrace 1254572, Maisara Caverns 1254559, Skyreach 159898, Pit of Saron
    1254555, Seat of the Triumvirate 1254551, Algeth'ar Academy 393273); this
    row was the only one wrong.

    Verification: 433 passed, 0 failed; luacheck 0 warnings / 0 errors in 14
    files; [2915] resolves to 1254553.

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

commit 55f55672898493285a0ce27dc9b423d5c659d807
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 22:10:03 2026 +0530

    data(teleports): add Nexus-Point Xenas, completing the Midnight S1 pool

    Reported in smoke testing: applying to a real Nexus-Point Xenas group showed
    no teleport button, while Keystone Polaris showed one for the same group.

    Not a regression from the remediation branch -- `git log main..HEAD --
    defaults/TeleportSpells.lua` was empty before this commit. Plain data
    staleness: NS.TeleportSpells had no row for mapID 2915, so GetTeleportSpell
    returned nil and ConfigureTeleportButton hid the button, which is the correct
    behaviour on a missing row. The reporter's own debug line is the diagnosis:

      [Frame] teleport spellID=nil known=nil (activity=1768 map=2915)

      [2915] = 1255391,  -- Nexus-Point Xenas

    mapID from the client's own [Apply] trace (map=2915). Spell ID confirmed in
    the client rather than from a web source, per docs/common-tasks.md's recipe:

      /dump C_Spell.GetSpellInfo(1255391)
      -> name="Teleport: Nexus-Point Xenas", spellID=1255391, iconID=135760

    Note the naming change, recorded in a comment above the Midnight block:
    Midnight issues these as "Teleport: <Dungeon>", not the older "Path of
    <Dungeon>". Only the ID is load-bearing -- the macro is built from the
    spell's own name at configure time -- but it is why the wiki has no
    "Path of Nexus-Point Xenas" page to check against.

    This was the only gap in the Midnight Season 1 M+ pool; the other seven
    (Windrunner Spire, Magisters' Terrace, Maisara Caverns, Algeth'ar Academy,
    Seat of the Triumvirate, Skyreach, Pit of Saron) were already present.
    Table now carries 74 rows.

    Verification: 433 passed, 0 failed; luacheck 0 warnings / 0 errors in 14
    files. Table loads under lua5.1 with [2915] resolving to 1255391.

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

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

    fix(frame): restore both click edges — the up edge is the one that casts

    Regression from [M4-24], reported in smoke testing: pressing the teleport
    button did nothing. No Lua error, and the PreClick trace still printed
    "teleport button pressed -> /cast Path of the Corrupted Foundry", which is
    what made it look like the button was working.

    Root cause. M4-24 narrowed RegisterForClicks("AnyUp", "AnyDown") to
    "AnyDown" alone, on the premise that a SecureActionButtonTemplate runs its
    secure action once per registered edge and two edges therefore cast twice.
    That premise was reasoned from reading the code and was never tested in a
    client. A bare SecureActionButtonTemplate with type="macro" does not run its
    macro on the down edge -- Blizzard's own action buttons cast on down because
    they opt into it, and this button inherits none of that. "AnyUp" was the edge
    executing the /cast all along; "AnyDown" only ever fed the PreClick trace.

    So the failure is silent by construction: the button still receives the down
    edge (the trace proves it) and casts nothing.

    WG-R-05 offered two resolutions -- register one click edge, OR document the
    PreClick gate as the reason both are needed. This takes the second, which is
    the one the evidence supports. Both edges restored; the comment at the
    registration now carries why, including the shape of the silent failure.

    tests/test_frame.lua: the M4-24 case asserted the edge COUNT was 1, so it
    pinned the regression rather than the behaviour -- a unit test cannot observe
    Blizzard's secure dispatch, so asserting the implementation detail was the
    wrong thing to assert. Inverted: it now requires both edges and says why
    losing either is undetectable in-game without pressing the button.

    Verification: 433 passed, 0 failed; luacheck 0 warnings / 0 errors in 14
    files. Planted violation: narrowing back to "AnyDown" alone -> 432/1 red;
    reverted -> 433/0 green.

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

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

    docs(architecture): record the events-frames-taint-§8 re-grade  [M4-27]

    DISPOSITION, not a fix. `M1-STD-11` resolved as option (a): §8's
    pre-formatting MUST NOT is now scoped to call sites whose arguments can
    reach a value read from one of the named combat-protected APIs, and is a
    SHOULD NOT everywhere else. No call site is converted.

    Sweep run against the live scoped rule. WhatGroup calls **none** of the
    trigger-set APIs:

      grep -rnE 'UnitGetTotalAbsorbs|UnitGetTotalHealAbsorbs|UnitGetIncomingHeals
      |UnitHealth|UnitHealthMax|UnitThreatSituation|UnitDetailedThreatSituation
      |C_UnitAuras|GetPlayerAuraBySpellID|"UNIT_AURA"'

    over core/ defaults/ locales/ modules/ settings/ returns zero — the addon
    reads LFG search-result data only (strings, integers, booleans from
    `C_LFGList`). So no pre-formatting site in this repo sits on a named API,
    none re-grades as a real fix, and every one of them is a SHOULD.
    `grep -rcE 'print\(\(".*"\):format' settings/` is 0 in all four files.

    Recorded as a register row in docs/ARCHITECTURE.md § Documented
    deviations. The row is deliberately NOT a blanket absolution: §8 keeps
    the no-global-`print` prohibition unqualified, so the two `pout`
    fallbacks in settings/Panel.lua and settings/Schema.lua are stated as
    standing on unreachability (the TOC loads core/WhatGroup.lua, which sets
    `WhatGroup._print`, before settings/) rather than on the re-grade, and
    the row carries a second re-check trigger that ends it the moment that
    branch becomes reachable.

    Closes WG-A-08.

    Verification: luacheck 0 warnings / 0 errors in 14 files; 433 passed,
    0 failed; lizard Warning cnt 0. Docs-only — no test case added, no
    planted violation applicable.

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

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

    fix(frame,debuglog): the §2 font fallback, one click edge, and the shared playstyle helper  [M4-24]

    Three unrelated one-offs, all traced to a finding.

    1. WG-A-13 (`debug-logging-§2`) — `core/DebugLogSetup.lua` handed
       `NS.FONT_MONO` to the library with no Blizzard fetch-failure fallback.
       The failure mode is silent: `SetFont` answers `false` when the client
       cannot load the file (a packager that dropped `media/fonts/`, a corrupt
       TTF, a case-mangled path) and does not raise, so the console would come
       up in a proportional font and the aligned `HH:MM:SS | [tag]` columns the
       monospace MUST exists for would be gone with nothing in the error log.
       Added `resolveConsoleFont`, which probes the path once at load with a
       `CreateFont` object and substitutes `Fonts\ARIALN.TTF` on failure. A
       Font object, not a throwaway frame: lightest thing carrying `SetFont`,
       never parented or shown, and it cannot disturb a shared Blizzard font.

    2. WG-R-05 — `modules/Frame.lua` registered the secure teleport button for
       `"AnyUp", "AnyDown"`. A SecureActionButtonTemplate runs its secure
       action once per REGISTERED edge, so one press fired the `/cast` macro
       twice, the second landing on a cast already going out. Now `"AnyDown"`
       only — the edge Blizzard's own action buttons cast on, and the edge the
       `PreClick` debug trace already gated for, so the trace is unchanged.

    3. WG-R-04 — `modules/Frame.lua`'s playstyle field open-coded what
       `Labels.GetPlaystyleLabel` already does, two lines below a call to its
       sibling `Labels.GetGroupTypeLabel`. Now calls the helper.

    Mock: `tests/wow_mock.lua` models `SetFont` (returns false for a path in
    the new `mock.fontFetchFails`) and `RegisterForClicks` (records the edges)
    rather than letting the PascalCase catch-all answer — in both cases the
    catch-all's answer is exactly the one that hides the behavior.

    Tests: three new cases in `test_debuglog.lua` (two) and `test_frame.lua`
    (one). docs/test-cases.md regenerated, README badge 430 -> 433. Docs:
    frame.md's field table and secure-button section, debug-console.md's
    descriptor list and new "fetch-failure fallback" subsection,
    ARCHITECTURE.md's vendored-font invariant, file-index.md's descriptor row.

    Closes WG-A-13, WG-R-04, WG-R-05.

    Verification: luacheck 0 warnings / 0 errors in 14 files; 433 passed,
    0 failed; lizard Warning cnt 0; test-cases.md diff empty.
    Deliberate violations, each shown red then reverted green:
      - `font = NS.FONT_MONO` (bypassing the probe)  -> 432/1 red, 433/0 green
      - `RegisterForClicks("AnyUp", "AnyDown")`      -> 432/1 red, 433/0 green
      - playstyle open-coded to `info.playstyleString or ""` -> 432/1 red,
        433/0 green

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

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

    docs(localization): ratify English-only, and make enUS.lua state the real surface  [M4-17]

    `localization-§3`'s routing SHOULD has two terminal compliant states —
    strings routed through `NS.L`, or an English-only decision recorded in the
    deviation register with a re-check trigger. WhatGroup was in neither: the
    decision lived only as prose in `scope.md` and a SCOPE comment, and five
    keys in `locales/enUS.lua` had no `L[...]` reader at all, which is the
    worse half — a key nothing looks up tells a translator a surface is
    covered when it is not.

    DISPOSITION on the five dead keys: four deleted, one wired.
    - `"Slash Commands"` is WIRED — `settings/Panel.lua` now calls
      `Helpers.Section(ctx, NS.L["Slash Commands"])`. Pure chrome, no
      structural role, behavior-identical under the fall-back metatable.
    - `"Ka0s WhatGroup"` and `"General"` are DELETED as unroutable. `"General"`
      is at once the options page id, the `group` key on three `Schema.lua`
      rows and the Blizzard subcategory label; translating the display copy
      alone would unmatch the schema and empty the page. `"Ka0s WhatGroup"`
      is the brand, carried by the TOC Title and the category label.
    - `"Defaults"` and the combat-refusal notice are DELETED as not ours —
      they are `LibKa0s-Options-1.0`'s `DEFAULTS_LABEL` / `COMBAT_REFUSED`,
      and a copy here would be a second source of truth that drifts.

    Added the `localization-§3` register row to `docs/ARCHITECTURE.md`
    § Documented deviations, naming all three unrouted classes, with
    Decided 2026-08-05 and re-check trigger "the first non-English locale
    file". Corrected the three places that claimed *every* authored string
    routes through `NS.L` (ARCHITECTURE.md invariants, load-order note and
    Non-goals; file-index.md's locale row; scope.md's localization bullet).

    Closes WG-R-06.

    Verification: luacheck 0 warnings / 0 errors in 14 files; 430 passed,
    0 failed; lizard Warning cnt 0. No new test case, so no planted
    violation; the existing `panel: the landing page renders a Slash
    Commands heading` case covers the wired call site.

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

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

    fix(options): register the settings category regardless of combat  [M4-01]

    `Settings.Register()` carried an `InCombatLockdown()` early-return as
    "defense in depth" atop `runConfig`'s open-path refusal. Registering a
    canvas Settings category is not a secure write and never taints, and
    `options-ui-§9` makes eager registration at load a MUST — so the guard
    bought nothing and cost the AddOns-list entry on any `/reload` taken in
    combat, since `_settingsRegistered` stayed clear and only a manual
    `/wg config` re-armed it.

    Removed the guard and the now-stale rationale comments in
    `settings/Panel.lua` and `core/WhatGroup.lua`; replaced them with the
    positive statement of why registration is ungated. Docs follow:
    ARCHITECTURE.md's Taint Notes and Invariants ("three combat guards" ->
    two), frame.md's Combat-defer list, settings-system.md's
    `Settings.Register()` section, file-index.md's Panel.lua row.

    Tests: `panel: registering during combat is refused and says why` and
    `panel: a combat-time bail still registers once combat ends` inverted
    into `panel: registering during combat still registers (options-ui-§9)`
    and `panel: a login taken in combat needs no second registration`;
    `lifecycle: /wg config registers the panel if login-in-combat skipped
    it` becomes `lifecycle: a login taken in combat still registers the
    panel`. docs/test-cases.md regenerated.

    Closes WG-A-12.

    Verification: luacheck 0 warnings / 0 errors in 14 files; 430 passed,
    0 failed; lizard Warning cnt 0; test-cases.md diff empty. Deliberate
    violation: reinstating the `InCombatLockdown()` early-return reddens
    both new cases (428 passed, 2 failed); reverted, back to 430/0.

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

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

    fix(settings): a read no longer materializes parent tables  [M4-03]

    settings/Schema.lua's `Resolve` walked the path creating an empty table for
    every missing intermediate segment, and BOTH `Helpers.Get` and `Helpers.RawSet`
    called it. So a read was a write: `Helpers.Get("brandnew.deep.leaf")` grew
    db.profile a `brandnew` table and a `brandnew.deep` table, which then
    round-tripped into SavedVariables — and made the very typo the caller was
    probing for indistinguishable from a real-but-empty branch on the next read.

    `Resolve` now takes a `create` flag. `Helpers.RawSet` passes true (a write must
    still be able to materialize a row nested under a table SavedVariables has never
    held — the existing "a write creates the intermediate tables it walks through"
    and "replaces a non-table intermediate" cases both still pass). `Helpers.Get`
    passes nothing and gets nil back, taking the existing "no path" branch, which
    also emits the Debug line that was already there for the no-db case.

    Renamed the existing case from "settings: Resolve creates the intermediate
    tables it walks through" to "settings: a write creates ..." — it exercises
    Helpers.Set, and after this change "Resolve creates" is only half true.

    Closes WG-R-03 (savedvariables-§2, "a read does not write").

    RED/GREEN, per the plan's plant-a-violation requirement:
      planted — reverted the guard to the old
        `if type(parent[k]) ~= "table" then parent[k] = {} end`
      red     — FAIL settings: Get on an unknown deep path returns nil and creates
                no table / tests/_kit/framework.lua:89: the read materialized a
                parent table (got <table>) — 429 passed, 1 failed, 430 total
      reverted— restored the create-flagged guard
      green   — PASS settings: Get on an unknown deep path returns nil and creates
                no table — 430 passed, 0 failed, 430 total

    Verification: luacheck . 0 warnings / 0 errors in 14 files;
    lua5.1 tests/run.lua 430 passed / 0 failed (429 → 430: the one added case,
    no case removed);
    lizard -l lua -x "./libs/*" -x "./tests/_kit/*" . Warning cnt 0;
    docs/test-cases.md regenerated with `lua5.1 tests/run.lua --list` and the
    CR-normalized diff against a fresh --list is empty; README tests badge moved
    429/429 → 430/430 in the same commit, as docs/testing.md requires.

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

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

    chore(packaging): complete the .pkgmeta ignore list  [M4-13]

    packaging (the live WowAddonStandards v2.22.0 section file, cited by bare
    filename — it carries no numbered subsections) MUSTs that the package ignore
    docs/, _dev/, tests/ and lockfiles, and its minimum template is
    .luacheckrc / .gitignore / docs / tests / _dev / "*.bak".

    The list carried the first four and omitted the last two. Added:
      - _dev      — the scratch dir the standard reserves
      - "*.bak"   — the lockfile/backup pattern; this is what "lockfiles" resolves
                    to in the canonical block, and the audit's own technical design
                    (docs/audits/2026-08-05/04_TECHNICAL_DESIGN.md, WG-36) reads it
                    the same way
      - .claude   — not in the template, but the acceptance is "the canonical block
                    plus every dev-only path present at root", and .claude/ is at
                    this repo's root

    No externals: block is added or implied; the vendored-libs note is unchanged.
    Parsed with yaml.safe_load to confirm the block is still valid YAML and the
    quoted glob survives as the string *.bak.

    Closes WG-A-07.

    Verification: luacheck . 0 warnings / 0 errors in 14 files;
    lua5.1 tests/run.lua 429 passed / 0 failed (baseline 429, unchanged);
    lizard -l lua -x "./libs/*" -x "./tests/_kit/*" . Warning cnt 0.

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

commit 396f9b763de022441098631f81a503a7c093c169
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 14:53:50 2026 +0530

    docs(architecture): carry every mandated heading  [M4-09]

    documentation-§3 mandates eight ARCHITECTURE.md sections — Overview, Module
    Map, Settings Schema, Message Bus, Slash Commands, Event Subscriptions, Taint
    Notes, Known Limitations — plus M3-08's "## Documented deviations".

    Added, all from the code rather than from memory:
      - Settings Schema — the ten profile rows with path/type/panel group, the
        structure-vs-values split (settings/Schema.lua vs defaults/Profile.lua
        NS.C), the global table, and the Get/Set write seam.
      - Message Bus — "there is none, because…", which C25 permits: grep for
        SendMessage/RegisterMessage over core modules settings defaults returns
        nothing, and _TryFireJoinNotify is the coordination seam instead.
      - Slash Commands — the eleven COMMANDS rows, host vs library ownership, the
        positional-triple shape, and the reserved-but-unregistered `perf` verb.
      - Event Subscriptions — the full surface from grep: GROUP_ROSTER_UPDATE,
        LFG_LIST_APPLICATION_STATUS_UPDATED, two one-shot PLAYER_REGEN_ENABLED
        frames, and the two file-load hooksecurefunc post-hooks.
      - Taint Notes — the no-AceHook rule, hooks-at-load/secure-frames-at-first-use,
        the three modules/Frame.lua combat guards, why category registration is not
        combat-gated, and the two C_Timer.After(0) taint hops.
      - Known Limitations — session-only capture, LFG-only capture, the
        hand-maintained teleport table, English-only, no profiler wiring.

    CITATION CORRECTION: WG-A-11 names four missing headings, but the file also
    lacked literal `Overview` and `Module Map` — it carried "What it does" and
    "Subsystems at a glance". The item's acceptance is that grep returns ALL EIGHT,
    so those two are renamed to the mandated names. Content is untouched by the
    rename and nothing linked either anchor (only #documented-deviations is linked,
    from performance.md and pending/LEDGER.md). docs/file-index.md's one-line
    description of the file is updated to match, so it does not become a C3 defect.

    Closes WG-A-11.

    Verification: luacheck . 0 warnings / 0 errors in 14 files;
    lua5.1 tests/run.lua 429 passed / 0 failed (baseline 429, unchanged);
    lizard -l lua -x "./libs/*" -x "./tests/_kit/*" . Warning cnt 0;
    grep '^## ' docs/ARCHITECTURE.md returns Overview, Module Map, Settings Schema,
    Message Bus, Slash Commands, Event Subscriptions, Taint Notes, Known
    Limitations and Documented deviations.

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

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

    docs(readme): drop the non-canonical "## Bundled libraries" heading  [M4-08]

    documentation-§1 permits twelve README sections in order: H1 title, badge row,
    logo, description, What's new, Screenshots, Usage, How <it> works, FAQ,
    Troubleshooting, Issues and feature requests, Version History.
    "## Bundled libraries" sat between "## How it works" and "## FAQ" as a
    thirteenth.

    The heading is gone; its paragraph moves verbatim into the description block
    above "## What's new", which is an unheaded permitted region. The paragraph had
    to be kept, not deleted: it carries the LibKa0s provenance line that
    tests/_kit/vendor_sync.lua reads out of README.md as the INPUT to the
    vendored-payload gate (DEFAULT_README pattern
    "[Bb]undles %[LibKa0s%]%b() (v[%d%.]+)"). Deleting the sentence would have
    turned both vendor-sync cases red.

    `grep '^## ' README.md` now returns exactly the canonical set in order.

    Closes WG-A-10.

    Verification: luacheck . 0 warnings / 0 errors in 14 files;
    lua5.1 tests/run.lua 429 passed / 0 failed (baseline 429), with
    "libs/LibKa0s is the LibKa0s release the README says this addon bundles" and
    "tests/_kit is the test kit that shipped with that release" both PASS, not SKIP;
    lizard -l lua -x "./libs/*" -x "./tests/_kit/*" . Warning cnt 0.

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

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

    docs(core): annotate NS.Util.format as a deliberate parity export  [M4-07]

    `NS.Util.format` is published on both the library path (core/CoreSetup.lua,
    `printer.Format`) and the degraded fallback branch, with zero production
    callers repo-wide. It is not dead by accident: it is the parity half of the
    printer surface, so the fallback exposes the same keys as the library path.

    Per C7 ("an exported symbol has a caller or a documented reason"), both
    publication sites now carry the reason explicitly, and each points at the
    other so a future removal takes both halves together. No behavior change.

    Closes WG-R-08.

    Verification: luacheck . 0 warnings / 0 errors in 14 files;
    lua5.1 tests/run.lua 429 passed / 0 failed (baseline 429, unchanged);
    lizard -l lua -x "./libs/*" -x "./tests/_kit/*" . Warning cnt 0.

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

commit 38642aecf923779a7c6eb7aa0dc4063b3d9b8226
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 13:06:38 2026 +0530

    docs(performance): claim the no-combat-path exemption, with the sweep  [M3-09]

    `performance-§12`. WhatGroup runs no code while the player is in combat, so
    the wiring MUST does not apply — but the claim is only an exemption when it
    is proven and recorded, which it was not.

    - docs/performance.md (new, one screen): the COMMITTED whole-repo sweep of
      `RegisterEvent` / `SetScript("OnUpdate"` / `C_Timer`, one row per hit
      naming the per-event work — zero OnUpdate handlers, zero repeating
      tickers, zero repeating timers; every scheduled item is one-shot or
      self-unregistering; the only handler reachable inside a combat window is
      GROUP_ROSTER_UPDATE, an IsInGroup() and three comparisons. Then criteria
      (b) and (c), the re-check trigger, and what the exemption does NOT excuse
      (whole-folder vendoring, the reserved `perf` verb, the `perf: skip` line).
    - docs/ARCHITECTURE.md: M3-08's register row re-cited from `performance-§1`
      to `performance-§12` — the exemption is a recorded exit, not an unmet MUST
      — with the sweep named as the Why and the trigger in the section's own
      words.
    - docs/file-index.md: performance.md listed in the docs/ read-on-demand set.

    Closes WG-A-01 … WG-A-06.

    Verification: docs/performance.md exists and the register carries exactly one
    `performance-§12` row with a Decided date and a re-check trigger; no `§N.M`
    notation introduced; luacheck 0/0; tests 429 passed, 0 failed; lizard
    Warning cnt 0; `--list` diff against docs/test-cases.md empty.

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

commit 8b65e0995d74e0466320f47af77b6eb73690d7ea
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 13:04:07 2026 +0530

    docs(architecture): open the documented-deviation register  [M3-08]

    Adds `## Documented deviations` to docs/ARCHITECTURE.md — the ninth mandated
    section and the SINGLE home for a ratified deviation (documentation-§3), in
    the exact row shape `| Rule | What differs | Why | Decided | Re-check
    trigger |`.

    Moves WhatGroup's one existing record in: the Perf decline reasoned at
    docs/pending/LEDGER.md's LIBKA0S-15 row (line 63), which was ratified with
    the user on 2026-08-02 and lived only in the ledger. A deviation that is not
    in the register is not ratified, so every audit cycle re-filed it as an open
    performance-§1 MUST failure. The ledger row now opens by naming the register
    as its home and keeps the long-form reasoning the Why cell cites.

    The row carries both structural reasons (no hot path; `suspend` would destroy
    capture data) and a re-check trigger that can be checked: the first OnUpdate,
    repeating ticker, or in-combat event handler doing more than occasional work.

    WG-A-13 is deliberately NOT here — nothing ratifies it, so it is a genuinely
    unmet debug-logging-§2 SHOULD and is code work, not a register row.

    Closes M3-08 (WhatGroup half).

    Verification: `grep -n "Documented deviations" docs/ARCHITECTURE.md`
    resolves; the row carries a `filename-§N` Rule, a Decided date and a
    Re-check trigger; luacheck 0/0; tests 429 passed, 0 failed; lizard Warning
    cnt 0.

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

commit 2f74372a65f103e2cabf9626e045d5835e7121b1
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 13:02:42 2026 +0530

    docs: retire the global §N.M notation across the repo  [M3-07]

    The widened revendor-standards notation sweep, excluding libs/, tests/_kit/,
    docs/audits/, docs/reviews/ and docs/automated-tests/ (all three docs/ paths
    are frozen evidence). 13 sites, comment- and prose-only; no code changed.

    - docs/smoke-tests.md, docs/wow-quirks.md, DEPENDENCIES.md: the §N and §N.M
      forms were this repo's own internal cross-references, not standards
      citations, but they are indistinguishable from the retired global notation
      by any sweep. Rewritten as "section N" / "sections N.M, ...". The
      `filename-§N` citations in the same files are the current form and are
      untouched.
    - .pkgmeta:4: `(§3.3, §13)` was a genuine reference in the retired global
      numbering — now `(packaging, library-stack-§3)`, matching packaging.md:5's
      own citation for the no-externals rule; `packaging` by bare filename since
      the rule is the section's whole subject.
    - docs/file-index.md:13: `compat-§1` is OUT OF RANGE — compat.md carries no
      numbered sections at all, so the reference could never resolve
      (documentation-§6 grades this a MUST, not the mechanical SHOULD). Replaced
      with the bare filename.

    Closes WG-A-15.

    Verification: `grep -rEn '§[0-9]+\.[0-9]'` excluding all five paths → 0;
    every remaining `filename-§N` range-checked against the fetched section
    file's local heading count, all in range; luacheck 0/0; tests 429 passed,
    0 failed; lizard Warning cnt 0.

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

commit 153ab39f6d58c81446deaf053364ec7235439a7b
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date:   Wed Aug 5 13:00:03 2026 +0530

    docs(testing): name the checkpoint on every gate statement  [M3-06]

    Sweep 3f. `no — recorded only` is true of a run and of a commit and false of
    the tag, so standing unqualified it reads as "perf and complexity gate
    nothing", which automated-tests-§3's release gate contradicts (testing-§6,
    automated-tests-§4).

    - docs/testing.md: the Gates? column splits into "Gates the run + the
      commit?" and "Gates the tag?", every row answering both; the paragraph
      below and the "At release, not at commit" paragraph now name the tag gate
      (all four suites at `pass` plus zero functions above CCN 15, evaluated by
      /wow-addon:bump-version from the run's manifest.json).
    - docs/automated-tests/README.md § "What gates, and what only records": same
      two-checkpoint table and prose.
    - docs/automated-tests/RESULTS.md: the LEAD-IN paragraph only, replaced by
      hand with the v8 runner's text — the runner writes that block only when
      RESULTS.md does not yet exist, so an existing file keeps the old wording
      until it is replaced. The trend rows are untouched; no dated bundle is
      touched.

    Closes WG-A-14.

    Verification: `grep -rn "never fail a run" docs CLAUDE.md` returns only
    checkpoint-qualified sentences outside the frozen docs/audits/ bundle;
    luacheck 0/0; tests 429 passed, 0 failed; lizard Warning cnt 0; `--list`
    diff against docs/test-cases.md empty (CR-normalized both sides).

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

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

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

    `git update-index --chmod=+x tests/_kit/run-automated-tests.sh`. The bit does
    not travel with `cp`, is not in the file's bytes, and this repo sets
    core.fileMode=false on a DrvFs tree, so `ls -l` reported rwxrwxrwx for a file
    the index called 100644 and nothing could see it (automated-tests-§2).

    Closes AT-A-05-class finding for this repo; the collection-wide item is M3-05.

    Verification: `git ls-files -s tests/_kit/run-automated-tests.sh` → 100755;
    `tests/_kit/run-automated-tests.sh --help` runs with no `bash` prefix;
    luacheck 0/0; tests 429 passed, 0 failed; lizard Warning cnt 0.

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

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

    test(libka0s): pin every LibKa0s stub surface as a set  [M3-04]

    Adds one `Kit.assertSurfaceParity` case per adopted LibKa0s seam — Core,
    DebugLog, Slash and Options — in tests/test_libka0s.lua's degraded section.
    Both arms come from a real load; the degraded arm is the existing partial
    file list (`skip = NO_LIBKA0S`), never a hand-stub of the member under test
    (testing-§8, anti-patterns #56). Each case names the grep that produces the
    live member list, and every `ignore` entry names the rule that makes the
    member live-only: debug-logging-§3 for the two formatters the DebugLog stub
    must not re-implement, options-ui-§1/§8 for the four layout scalars, plus
    AceGUI and the three library-internal renderers WhatGroup never calls.

    Nothing reddened on adoption — WhatGroup's four stubs were already whole —
    so no seam code changed. README test badge and docs/test-cases.md
    regenerated for the four new cases.

    Closes M3-04 (WhatGroup half).

    Verification: luacheck 0/0; `lua5.1 tests/run.lua` 429 passed, 0 failed
    (425 + 4); lizard Warning cnt 0; `--list` diff against docs/test-cases.md
    empty (CR-normalized both sides). Each case shown RED against a planted
    deletion of one stub member and green after revert: Core/MakeCloseButton,
    DebugLog/IsShown, Slash/SetRowAnnotator, Options/RenderRows.

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

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

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

    Re-vendors BOTH payloads from the LibKa0s v1.8.0 tag in one commit, because the
    gate compares file SETS and reads the README provenance line as its input:
      * cp -r <LibKa0s>/LibKa0s/. libs/LibKa0s/   (6 files changed)
      * cp -r <LibKa0s>/testkit/.  tests/_kit/    (4 changed, vendor_sync.lua new)
    and bumps README.md's provenance line from v1.7.0 to v1.8.0. A kit-only copy
    would point the gate at a tag whose libs/LibKa0s/ bytes this repo does not hold.

    M3-02: WhatGroup had no vendored-payload gate. It gains one as a ~10-line
    VendorSync.register(_G.WHATGROUP_TEST, {}) call into tests/_kit/vendor_sync.lua,
    with the default case names so they match the other seven repos, declared in
    tests/run.lua's suite list. The AbsorbTracker header explaining the single
    CR strip on the working-tree side is carried in; the strip itself now lives in
    the kit module, where it is correct and stays (the other side is a `git show`
    blob, which is LF, while this working tree is CRLF).

    Also closes WG-R-09: the British spellings in libs/LibKa0s/ close on the
    library-half re-vendor with no local edit (M3-14a) — grep now returns 0.

    Verification:
      luacheck .                                        0 warnings / 0 errors, 14 files
      lua5.1 tests/run.lua                              425 passed, 0 failed (was 423; +2, the gate)
      lizard -l lua -x ./libs/* -x ./tests/_kit/* .     Warning cnt 0
      diff -r <LibKa0s>/LibKa0s/ libs/LibKa0s/          empty
      diff -r <LibKa0s>/testkit/ tests/_kit/            empty
      --list vs docs/test-cases.md (CR-normalized)      empty
      suite inventory pre-flight (14 on disk / 14 declared)  balanced
      planted content byte in libs/LibKa0s/Core.lua     -> 1 failed (case #1)   reverted -> green
      planted extra tests/_kit/PLANTED.txt              -> 1 failed (case #2)   reverted -> green
      planted stale provenance line v1.6.0              -> 2 failed            reverted -> green
      bogus sibling opt (in place of moving ../LibKa0s, which is racy while other
      agents run against it)                            -> 2 SKIP with reason, exit 0
    README [tests] badge moved 423 -> 425. Exec bit deliberately NOT set here (M3-05).

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

This mod has no additional files