1.3.0-release
What's new
commit ed88759f694600b3ad6aaf3281f6a4b60c795622
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Thu Sep 10 23:54:57 2026 +0530
Release 1.3.0
The global cooldown is now attributed per spell rather than per log line (#15),
so icons stop reading as ready when they are not; the cast bar stopped minting
a closure per cast; and the download is about 7.5 MB lighter. 176 commits since
the 1.2.1 tag.
Release gate passed on all four suites at 1fee098: luacheck 0/0 across 94
files, 870 tests green, 6 perf scenarios, zero functions above CCN 15. The
largest source growth of the nine this cycle (+359 NLOC over 20 new functions),
and the averages confirm it is new code rather than existing code thickening.
Evidence: docs/automated-tests/20260910-234511/.
What's new is deliberately not recreated (documentation-§1, standard v2.42.0).
Badges follow: WoW to 12.1.0, tests to the 870 measured.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4Xy2KrRa8NmD1843rgKnE
commit 566f6fc3eddb5fe20b5e642256770f4fe1abef24
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Fri Sep 11 00:55:19 2026 +0530
sync-docs: three dead publications, and a gate that guarded nothing
Const.PANEL_DEFAULTS_W, Helpers.PrintSchemaError and Helpers.FireOnChange had no
callers. The first two are simple deletions; FireOnChange took its file-local
with it, because the publication was the local's only consumer and luacheck said
so the moment the line went. That in turn left tests/test_locale.lua excusing an
'onChange for ' literal that no longer exists in settings/Panel.lua, and the
residue gate caught it -- the entry is dropped. Panel_Render.lua's entry stays;
that literal is still there.
tests/test_spelling.lua asserted that libs/LibKa0s/LibKa0s.lua is not reached by
the spelling scan. No such file exists in the vendored payload, so the assertion
passed on a path that could never be scanned -- vacuously true, guarding nothing.
It now names libs/LibKa0s/Core.lua, which is real.
tests/test_locale.lua pointed a reader at docs/debug.md for the DIAGNOSTIC class
wording; ARCHITECTURE.md:214 files that doc Not applicable and it does not exist.
The clause is gone.
Target interface to 120100 across DEPENDENCIES.md, ARCHITECTURE.md, scope.md and
module-map.md. docs/test-cases.md regenerated -- it was one case stale.
luacheck clean at 94 files, 870 tests green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4Xy2KrRa8NmD1843rgKnE
commit 1fee09855098b757a525a7968cdeb5c2dc06c806
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Thu Sep 10 23:44:53 2026 +0530
Target patch 12.1.0 — the TOC interface becomes 120100
Retail moved to 12.1.0, and an addon whose TOC still declares 120007 shows as
out of date in the client's addon list even though nothing in it broke.
The line is the only thing that changed. LibKa0s needed nothing alongside it:
it ships no TOC of its own, reads the client build from GetBuildInfo() at
runtime rather than declaring one, and the harness vendored as tests/_kit/
carries no build number at all — so no re-vendor was involved.
Verified before committing: luacheck clean and the headless suite green in
every repo.
Living documentation still says 120007 / 12.0.7 in the README badge and
DEPENDENCIES.md, and in several repos ARCHITECTURE.md, scope.md, module-map.md
and smoke-tests.md as well. That sweep is /wow-addon:sync-docs' job, not this
one-line change's. Comments recording measured 12.0.7 behavior stay as they
are — they are still true.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4Xy2KrRa8NmD1843rgKnE
commit 8d76ff16d3362434b61a157fd262bd581a4f3061
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Thu Sep 10 23:26:19 2026 +0530
README: drop the logo and the five settings-panel screenshots
Five shots of a settings panel tell a reader nothing about whether the addon
is worth installing; the zoomed grid-and-cast-bar shot does that on its own.
The logo block goes too, since the CurseForge page carries it. The opening
question folds into the paragraph that answers it rather than standing alone.
The logo bitmap is refreshed to match the upload.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4Xy2KrRa8NmD1843rgKnE
commit 0d30e0610cc52506f3469af8c610e1cce9e9596f
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Thu Sep 10 03:31:45 2026 +0530
media: refresh the screenshot set
Local source images only. Every README serves its screenshots from the
CurseForge CDN, so nothing here is referenced by a doc and no page
changes until the new set is uploaded there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVx7o2xdz6H1jH2CZviDw8
commit 1e59f87a21b939b2d0f60f4e3cb13b6402445837
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Thu Sep 10 02:51:40 2026 +0530
README: drop `## What's new` (standard v2.42.0)
documentation-§1 no longer carries the section. It was bound to the top
`## Version History` row -- the two "MUST agree" and rolled forward
together on every bump -- so its content was a copy of another MUST's
content by definition, and the only work it could generate was keeping
the copy true. Version History is now the addon's single player-facing
history.
Anti-pattern #40 is retired in place upstream; the item list renumbers
6-13 to 5-12.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVx7o2xdz6H1jH2CZviDw8
commit 2eb8755b8ee4dcec1a631a9c8f24a545ed8c9232
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Wed Sep 9 21:43:07 2026 +0530
README: adopt the standard's prose Usage section (v2.41.0)
documentation-§1 item 7 no longer mandates the two tables that made
Usage a contents page. The `Command | What it does` table said what
exists without ever saying what to do, and it was a second copy of a
list the addon already generates from NS.COMMANDS for `/<slash> help`,
so it could only be in lockstep or wrong. The `Tab | Covers` table has
moved to docs/settings-panel.md, its canonical home, where the finer
page-tab-row tree already sat beneath it.
Usage is now prose in the order a new player meets the addon: how the
display is shown, hidden, moved and locked, what preview or test mode
is for, what the chrome's controls do, and the core interactions. It
closes on one sentence pointing at Settings > AddOns and the slash
command list.
The whole README was humanized in the same pass -- em dashes cut,
`**Bold lead.**` bullets removed, see-saw pairs and repeated rhetorical
moves broken up, sentence and paragraph length varied.
Reviewed by an adversarial pass that checked every factual claim
against source; the corrections it found are folded in.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVx7o2xdz6H1jH2CZviDw8
commit a3ee0856fa22f93b77f2194277108211debb86db
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Wed Sep 9 19:46:22 2026 +0530
README: the Troubleshooting table takes the header the standard names
`documentation-§1` item 10 states the section as a **Symptom | Fix**
table, and this one said `| Symptom | What to check |`. Header row only -- not a symptom or a
fix is reworded.
Found while restyling Ka0s Multi Meters, whose FAQ and Troubleshooting
were paragraphs rather than tables. Nine addons carry both sections; the
FAQ header was already unanimous, and four of the nine had drifted on
this one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVx7o2xdz6H1jH2CZviDw8
commit 70b17b14a2ec1b32a5ea2626a8e5c73ff8f2a3d5
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Wed Sep 9 03:21:46 2026 +0530
perf(cooldowns): bracket the SPELL_STATE publish, and observe the nesting
The 20260909-014035 capture put a 9.64 ms pass inside Cooldowns:Refresh and
outside every child bracket, and the buckets could not say where. This is what
was in the gap.
`spellState` brackets the SUBSCRIBER (IconGrid:OnSpellState); nothing bracketed
the NS:SendMessage around it — CallbackHandler's dispatch plus the six-field
payload table the loop allocates per emitting spell, ~1590 of them across that
capture's 810 passes. A collection landing inside the bracket is charged to
whichever pass was unlucky, which is the shape a lone outlier with no child
spike actually has; tests/perf.lua measures spellPoll at 528 bytes/iter against
spellState's 209. `stateEmit` now brackets the publish and the constructor with
it, so the chain is spellPoll > stateEmit > spellState > iconApply and
spellPoll minus its two children is the bare loop.
NOT glowGate, which the capture's write-up first proposed: RefreshAllGlows is
not reachable from Cooldowns:Refresh. The only glow work on the poll path is
Icon:UpdateGlow, already inside iconApply. glowGate is declared anyway on its
own merits — four of its five call sites sit inside no bracket, so
LibCustomGlow's cost has been invisible in every capture — and declares no
parent, for the reason PerfSetup.lua already records for `visibility`.
parentKey is threaded through pollSpell, stateEmit and iconApply so containment
is measured rather than asserted. PollSpell and Icon:Apply take it as an
argument rather than naming a parent themselves: Rebuild polls the same spells
from outside spellPoll, and two of Apply's three callers run under no bracket,
so a hard-coded parent would report containment in a pass that never ran — the
same unverified claim the change exists to end.
tests/test_perfsetup.lua pins both directions, including that a Rebuild-driven
poll claims no parent. Red under dropping any third argument. luacheck 0/0,
870 tests pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nbSbqagQQGGgyab6o86Mj
commit f704e4123c56b8e9711dc210ee43c82612cea2e3
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Wed Sep 9 03:21:31 2026 +0530
perf: the 2026-09-09 capture — one 9.64 ms pass, unattributable
A party-of-five run in Nexus-Point Xenas, v1.2.1. The frame-time delta came
back at +0.04 ms/frame, roughly a seventh of the instrument's floor, so it
resolves nothing; the buckets are the result.
Accounted cost is 3.674 ms per second of combat, which is unremarkable. The
finding is the tail: one spellPoll pass cost 9.6400 ms — 37x its own mean and
69.7% of a 13.83 ms frame — with no child bucket anywhere near it (pollSpell
max 0.2961, spellState 0.2135, iconApply 0.1948). The cost was inside
Cooldowns:Refresh and outside every bracket it contained, so the record can
locate the hitch to a function and no further.
Every nested row reads "declares itself within X — not observed", so the
residual that isolates the gap is arithmetic over an unverified tree. Both
gaps are closed in the commit that follows this one.
Line endings normalised to the CRLF .gitattributes declares; the bundle was
first written with bare LF, which the eol suite catches.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nbSbqagQQGGgyab6o86Mj
commit d81d2a582fa97c5d4f8afc9a109514eb136311fa
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Wed Sep 9 01:40:50 2026 +0530
re-vendor LibKa0s v1.29.0 — the JSON dump folds into the report step
`report` printed the summary and `dump` printed the run as one line of JSON.
Both went to the same log, both described the same finished run, and
perf-analysis asks for both -- so two verbs and two panel rows meant a second
click, a second thing to remember, and a run reported without its dump was the
easy mistake to make. `report` writes both now, JSON last, because the summary
is what you read and the JSON is what you copy.
`perf dump` is no longer a verb and `Progress().dump` is gone. Folded rather
than aliased: an alias is the duplication the fold removes, and the unknown-verb
path prints the usage, where `report` now says it renders the JSON too.
Payload and kit copied whole from a clean checkout of the tag; the CLAUDE.md
provenance line rolls in this commit. Kit revision unchanged at 15.
diff -r against a clean v1.29.0 checkout: byte-empty, both payloads.
869 passed, 0 failed, 0 skipped, 869 total
Total: 0 warnings / 0 errors in 94 files
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nbSbqagQQGGgyab6o86Mj
commit fca318fc00795ffab481d779704452a5b80467e4
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Wed Sep 9 01:27:11 2026 +0530
re-vendor LibKa0s v1.28.0 — the perf usage block renders correctly
Perf.lua 9 only. The usage block printed `<start|measure|finish|canceleport|
dump|showideoggle>`: the verbs are pipe-separated and the client reads `|r` as
a color reset, `|h` as a hyperlink and `|t` as the end of a texture, so it ate
all three and the words fused. The eaten `|r` also swallowed the reset closing
the gold run, which is why the line stayed yellow. The pipes are doubled now.
The block also hand-aligned a second column with spaces and wrapped each
description onto a continuation line, which a proportional chat font cannot
honor. It is one row per verb through the collection's own FormatRow now, the
same formatter the slash-command help uses.
Payload and kit copied whole from a clean checkout of the tag, not from the
library's working tree, and the CLAUDE.md provenance line rolls in this commit.
Kit revision is unchanged at 15. No member added, removed or renamed, so nothing
in this addon had to adopt anything.
diff -r against a clean v1.28.0 checkout: byte-empty, both payloads.
869 passed, 0 failed, 0 skipped, 869 total
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nbSbqagQQGGgyab6o86Mj
commit c79d351a714a300fe3d8cd01932c3cabcc92fbc8
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Wed Sep 9 00:35:51 2026 +0530
fix(cooldowns): attribute the GCD per spell, not per log line (#15)
The first cut marked "was a GCD running when this line was written?" when the
question is "is this line the GCD's doing?", and a second live trace showed the
difference inside a minute.
Two failures, opposite directions. A GCD flips a spell active NOW and back to
ready ~1.5s LATER, by which point the GCD has ended -- so every closing half of
the churn came out unmarked and half the noise stayed unlabeled:
00:30:16 | 4/5 changed: active=[207167,108199,221562,49576] (gcd)
00:30:18 | 4/5 changed: ready=[207167,108199,221562,49576]
And worse, in the other direction:
00:30:35 | 1/5 changed: ready=[47528] (gcd)
Mind Freeze coming off its own 15s cooldown -- the single most interesting line
in the log -- labeled as noise because an unrelated GCD happened to be running
at that instant. Mind Freeze is OFF the global cooldown, so nothing about that
transition was the GCD's doing.
Attribution is per spell now and carried across polls. A spell that goes down
while a GCD runs is attributed to it; a spell still down after the GCD has
ended has the attribution withdrawn, because that is a real cooldown wearing a
GCD's clothes. A transition to ready is attributed to whatever put the spell
DOWN, not to the flag that happens to be set when it comes back up. The line is
marked only when EVERY logged transition is the GCD's -- one real transition
disqualifies it, because "(gcd)" on a line carrying a real cooldown reads as
"skip me" over the one thing worth reading.
The withdrawal had to go in the not-changed branch, which is the part that is
easy to miss: a spell parked on a real cooldown does not change from poll to
poll, so that is the only place its attribution can ever be corrected. Without
it the mixed case stays marked and the fix looks right in three cases out of
four.
Written in place on the record already held -- no allocation -- and the whole
attribution path is inside `if dbg`, so nothing runs when the console is not
listening. lizard still reports 0 warnings, so M4-25's release gate holds.
Three cases, all watched red against the first cut rather than against the
original code: the closing half of a GCD, a real cooldown coinciding with one,
and a line mixing both.
869 passed / 0 failed (was 866), luacheck 0/0 in 94 files.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nbSbqagQQGGgyab6o86Mj
commit e95f204397380388affea04f4ee7b35b6e420828
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Wed Sep 9 00:28:29 2026 +0530
fix(cooldowns): mark the debug lines the global cooldown explains (#15)
Found running smoke session 1 step 1.7 on live Retail. Seventeen [Cooldowns]
lines in a 45-second fight, and two of them were cooldowns:
18:08 | 5/5 changed: ready=[207167,108199,221562,49576] active=[47528]
18:23 | 1/5 changed: ready=[47528]
Mind Freeze on and off its 15s cooldown, exactly right. The other fifteen are
the global cooldown -- four utility spells flipping active <-> ready in lockstep
on every press, twice inside the same second at 18:13 and again at 18:18.
buildSpellState derives both fields from the legacy active flag, and the header
at :50 says what that flag covers: "real CD or just GCD". MaterialChange keys on
exactly those two fields, so a GCD is material by its own test and gets a line.
MARKED, NOT SUPPRESSED, and that is forced rather than preferred. The C-side
curve evaluation that separates a GCD from a real cooldown cannot hand its
answer back into a Lua `if`, and every duration involved is secret in combat --
the same wall the header already documents for the icon path. Worse, any
time-based heuristic that hid the churn would hide a real cooldown starting too:
the 5/5 line above, the single most interesting line in the log, IS a GCD line.
Spell 61304's plain-bool active flag is the one value in reach that can be
branched on at all, so the line reports which it was and the reader judges.
Scope stated honestly, because the issue overstated it first: this is not the
flood MaterialChange was written to stop. That was "~10 identical lines per
second"; this is 0.33/s and the lines are not identical. Nothing was broken and
nothing was hidden by volume. What it cost was signal -- in the one situation
the console exists for, fifteen of seventeen lines said nothing about whether an
interrupt was available.
The mock's GetSpellCooldown ignored its spellID and answered one fixed table for
every spell, so the GCD could not be expressed at all -- which is why no case
could have caught this. It is per-spell now, seeded through mocks.spellCooldowns,
with unseeded ids answering the old inert shape so no existing suite moves.
Two cases: a flip the GCD explains carries the marker, and one it does not carry
none -- a marker on every line carries as much information as none at all. The
first was watched red.
Rendering is untouched: IconGrid still separates GCD from real cooldown C-side
against a curve, which is the only place that distinction can be made correctly.
866 passed / 0 failed (was 864), luacheck 0/0 in 94 files, lizard 0 warnings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nbSbqagQQGGgyab6o86Mj
commit 06ec1755d76c47c57f8d325613e5bd933896a6f5
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Sep 8 22:18:20 2026 +0530
M5-06-C1: the lint citations name a section that does not exist
The 2026-09-08 standards audit found these, and this cycle wrote them. M4c-03,
M4c-04 and M4c-06 removed the blanket luacheck ignores across the collection and
cited the rule as `lint-§1` in the configs, the gate cases and the testing docs.
standards/lint.md carries exactly one heading and no numbered subsections, so
`lint-§1` resolves to nothing. The house rule for a section-less file is the bare
filename -- the same ruling M1-STD-04 made for standalone-windows.md, whose
line-number citations exist for this reason.
Seventy citations across nine repositories, all authored 2026-09-08, all now
reading `lint.md`. No behaviour changes and no count moves: every hit is a
comment or an assertion message.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nbSbqagQQGGgyab6o86Mj
commit 2095532bd4b7a51bd39b8b319bcab33f467a20ee
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Sep 8 21:46:18 2026 +0530
M5-06: the fresh audit against v2.39.0, and three findings only it can see
A read-only compliance measurement of KickCD at 03f3b9a against the Ka0s WoW
Addon Standard v2.39.0 (2026-09-07), written to docs/audits/2026-09-08/. No
addon code, TOC, config or document outside that folder was changed, and
docs/audits/2026-09-07/ was not touched.
The standard was resolved before anything was measured: AUDIT.md,
standards/STANDARDS.md (line 1 read v2.39.0) and all 26 section files the
Sections list links, fetched with curl and read verbatim. Each was diffed
against the 2026-09-07 copy so the run knows which rules moved -- fifteen
sections changed, and three of this bundle's four new findings exist only
against the amended text.
Eight roots, no dependents. High 0, Medium 0, Low 5, Info 3; four MUST
failures, every one of them Low because no user, no KickCDDB and no session
can reach any of them today. The 2026-09-07 run filed eleven, of which nine
are closed.
The two that this cycle was supposed to close and did not:
- KICKCD-A-08. M4-20 fixed the single site the audit cited
(modules/Castbar_Debug.lua) and nothing swept for the class. Three
`or _G.print` fallback arms survive at core/KickCD.lua:108,
core/Compat.lua:452 and modules/Cooldowns.lua:538. The audit
under-scoped and the fix followed the audit.
- KICKCD-A-02. The MUST half closed -- five load-bearing TOC positions,
five annotations naming what resolves. The SHOULD half did not:
# Locales, # Defaults, # Modules and the # Core remainder still carry no
conventional note, which M4-12's own commit message says was deliberate.
The three that only the amended rules make visible:
- KICKCD-B-01. documentation-§3 now specifies ### Verification and record
and fixes its membership at exactly six docs, with perf-analysis/README.md
registering in ### Conditional instead. KickCD's fourth table has seven
rows. The table had no specification at all before this cycle.
- KICKCD-B-02. localization-§5 now publishes the canonical BRITISH/ALLOWED
lists and specifies the four exclusions as *frozen dated bundles*.
tests/test_spelling.lua carries both lists whole -- 91 and 30, matching
the published block entry for entry -- and then excludes the whole
docs/perf-analysis/ directory, including the live README.md that
documentation-§3 calls "the one file in the store that is rewritten". Two
British spellings sit there un-caught, at :32 and :33.
- KICKCD-B-03. audit-review-history's third MUST -- evaluate every register
row's re-check trigger against the tree -- is new this cycle. It finds
that docs/ARCHITECTURE.md:248 says "Two profile migrators run off the
stored shape" while core/Database.lua:598-601 runs three, and has done
since e143516 on 2026-07-16, the row's own Decided date. Three audits
accepted the row without re-reading the count.
Measured, not reasoned: luacheck 0/0 over 94 files with the test tree in
scope; 864 cases green; lizard's verbatim invocation reports zero warnings
against the record's zero, so the release gate KICKCD-R-02 blocked is clear;
the line-ending working-tree sweep returns 0 against the previous run's 8; and
both diff -r gates against LibKa0s v1.27.0 -- the ship folder and the test kit
-- are empty. Every file:line in the bundle was re-read and quoted beside its
citation, and every count carries the command and the scope that produced it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nbSbqagQQGGgyab6o86Mj
commit 86744a46c804dd62e04ef75873ee3c401faf7c1b
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Sep 8 20:14:34 2026 +0530
M4c-06: the blanket ignore goes, and thirty-two of the sixty-one were real
`.luacheckrc:12-16` carried `ignore = { "212/self", "212/event", "211/addonName" }`.
This one is worth naming precisely, because it does not look like the blanket the
other two repos carried. Every entry was already in luacheck's narrow
`<code>/<variable>` form. That is the narrow SPELLING, and it sat at the top
level, which is the widest SCOPE there is: those three names were silenced in all
93 files, including every file with no business producing them. `M4-11` is about
scope, not spelling -- an ignore that silences the wall reads as coverage and
provides none.
It provided none here, and the measurement is in the tree. Removing the three
lines took `luacheck .` from 0/0 to **61 warnings** -- 29 `211/addonName` and 32
`212/self`, no `542` anywhere -- and **thirty-two of the sixty-one were not
conventions at all**:
* Twenty-nine files opened `local addonName, NS = ...` over a folder name they
never read. Five files in this addon do read it -- CoreSetup, EnvSetup,
MediaSetup, DebugLogSetup and PerfSetup, each handing it to a vendored LibKa0s
payload that cannot infer which folder it was copied into. The other
twenty-nine had the line because it was copied. They now open
`local _, NS = ...`, which is how core/PoolSetup.lua already spelt it.
* Two receivers in the test tree were named and never read: the mock module
method in tests/test_util.lua and `t.SendMessage` in tests/wow_mock.lua. Both
are `_` now, which keeps the arity the mocked signature owes while dropping
the claim that a sender is consulted.
* One dead function. settings/Slash.lua carried
`function NS.Slash:PrintHelp() return NS.Slash.cli:PrintHelp() end` and
nothing called it: core/KickCD.lua:253 reaches `NS.Slash.cli:PrintHelp()`
directly, behind the same "did settings/ load at all" guard the forwarder
carried, and `NS` is private (there is no `_G.KickCD`), so nothing outside
could reach it either. The blanket is what hid it -- with the receiver
unreported the line read like the third member of a trio beside `LandingRows`
and `OnSlash`, both of which are called. Deleted, with a comment in its place
saying why there are two forwarders and not three.
`212/event` deserves its own line: it matched **nothing**. No file in this tree
produces an unused `event`. It was carried for a convention that is not here, and
while a suppression is switched on everywhere nobody can tell a live one from a
stale one. That is the second cost of a blanket, after the one the rule names.
Twenty-nine remain, all `212/self`, answered by nine per-file `files[...]`
stanzas: the two version-gated Database migrators reached through the
`migrations` scaffold, `NS:OnEnable`, the four AceAddon module objects whose
methods every caller reaches with the colon, the text-icon registry pair in
IconGrid_Render, the `LibKa0s-Slash-1.0` degradation stub and the two `NS.Slash`
forwarders, and the spells page. Each carries a comment naming the obligation
that forces the receiver. `IconGrid:OnSpellState` is the sharpest of them: it is
registered BY NAME at modules/IconGrid.lua:769, and AceEvent-3.0 invokes a
name-registered handler as `self[method](self, ...)`, so the receiver is not this
addon's choice at all.
The narrowing was measured, and measured for the right thing, which took two
tries. A dead argument under a NEW name always reported here -- the old entries
named their variables, so adding a `deadArg` to `Castbar:GetCastbarFrame` goes
red under both configs and proves nothing. The claim that quoted it was written
into this file's comments and then removed, because it was false. What the old
config actually swallowed is the NAME it listed, in every file: appending
`function NS.Util:DeadProbe() return 1 end` to core/Util.lua reports
`core/Util.lua:429:17: (W212) unused argument 'self'` under the new config and
reports nothing under the old one, re-linted with `luacheck --config` against the
blanket. The twenty-nine `addonName` headers are the same fact at scale -- they
sat under a green 0/0 for the life of the addon.
The gate is the other half, and `M4c-03` and `M4c-04` are the shape it follows: a
top-level `ignore` is one line for anyone to re-add. tests/test_lintconfig.lua
loads `.luacheckrc` as Lua under a sandbox that auto-creates tables the way
luacheck's own config loader does, so it inspects the table luacheck obeys rather
than text a different spelling would slip past. Four cases, and all four were
watched red in the working tree before this commit: a top-level `ignore`; a class
switched off wholesale (`unused_args = false` and eight relatives); a `files[...]`
ignore whose key is a directory and whose entry names no variable; and a bare
`-- luacheck: ignore` anywhere in tracked Lua. It fails rather than skips when it
cannot look, the bargain test_doc_structure and test_eol already strike here.
Documentation moved with the code. docs/common-tasks.md taught the retired
convention in the most direct way possible -- it described the three entries as
"allowances true everywhere" and closed with "prefer the inline form" -- and now
says there is no top-level ignore, what removing it exposed, and that fixing the
code beats either suppression. docs/ARCHITECTURE.md said "every source file opens
with `local addonName, NS = ...`" over a code block spelling it that way, and now
names the five that read the folder name and the thirty that write `_`. CLAUDE.md
carried the same header inline. docs/testing.md gains the gate as the sixth of its
source-scan guards. DEPENDENCIES.md's `.luacheckrc:10-43` citation for the
`read_globals` block was stale before this change and is now `:47-80`.
Verification, observed. `luacheck .` 0 warnings / 0 errors in 94 files.
`lua tests/run.lua` 864 passed, 0 failed, 0 skipped -- 860 before, plus the gate's
four. docs/test-cases.md regenerated through `--list` and `diff` against it is
empty; its Totals row reads 864 and the README `[tests]` badge moves 860 to 864 in
this commit. No version anywhere: Decision 5 stands and the badge is a count.
No smoke step, and none is claimed. Thirty shipped files changed and every change
in them is inert: twenty-nine rename a local nothing read, and the thirtieth
deletes a member nothing called. Lua does not care what an unread local is named,
and a table field with no reader has no observable removal. Nothing else in the
change reaches the client -- `.luacheckrc` and tests/test_lintconfig.lua are not
packaged, and the rest is documentation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nbSbqagQQGGgyab6o86Mj
commit 86744a46c804dd62e04ef75873ee3c401faf7c1b
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Sep 8 20:14:34 2026 +0530
M4c-06: the blanket ignore goes, and thirty-two of the sixty-one were real
`.luacheckrc:12-16` carried `ignore = { "212/self", "212/event", "211/addonName" }`.
This one is worth naming precisely, because it does not look like the blanket the
other two repos carried. Every entry was already in luacheck's narrow
`<code>/<variable>` form. That is the narrow SPELLING, and it sat at the top
level, which is the widest SCOPE there is: those three names were silenced in all
93 files, including every file with no business producing them. `M4-11` is about
scope, not spelling -- an ignore that silences the wall reads as coverage and
provides none.
It provided none here, and the measurement is in the tree. Removing the three
lines took `luacheck .` from 0/0 to **61 warnings** -- 29 `211/addonName` and 32
`212/self`, no `542` anywhere -- and **thirty-two of the sixty-one were not
conventions at all**:
* Twenty-nine files opened `local addonName, NS = ...` over a folder name they
never read. Five files in this addon do read it -- CoreSetup, EnvSetup,
MediaSetup, DebugLogSetup and PerfSetup, each handing it to a vendored LibKa0s
payload that cannot infer which folder it was copied into. The other
twenty-nine had the line because it was copied. They now open
`local _, NS = ...`, which is how core/PoolSetup.lua already spelt it.
* Two receivers in the test tree were named and never read: the mock module
method in tests/test_util.lua and `t.SendMessage` in tests/wow_mock.lua. Both
are `_` now, which keeps the arity the mocked signature owes while dropping
the claim that a sender is consulted.
* One dead function. settings/Slash.lua carried
`function NS.Slash:PrintHelp() return NS.Slash.cli:PrintHelp() end` and
nothing called it: core/KickCD.lua:253 reaches `NS.Slash.cli:PrintHelp()`
directly, behind the same "did settings/ load at all" guard the forwarder
carried, and `NS` is private (there is no `_G.KickCD`), so nothing outside
could reach it either. The blanket is what hid it -- with the receiver
unreported the line read like the third member of a trio beside `LandingRows`
and `OnSlash`, both of which are called. Deleted, with a comment in its place
saying why there are two forwarders and not three.
`212/event` deserves its own line: it matched **nothing**. No file in this tree
produces an unused `event`. It was carried for a convention that is not here, and
while a suppression is switched on everywhere nobody can tell a live one from a
stale one. That is the second cost of a blanket, after the one the rule names.
Twenty-nine remain, all `212/self`, answered by nine per-file `files[...]`
stanzas: the two version-gated Database migrators reached through the
`migrations` scaffold, `NS:OnEnable`, the four AceAddon module objects whose
methods every caller reaches with the colon, the text-icon registry pair in
IconGrid_Render, the `LibKa0s-Slash-1.0` degradation stub and the two `NS.Slash`
forwarders, and the spells page. Each carries a comment naming the obligation
that forces the receiver. `IconGrid:OnSpellState` is the sharpest of them: it is
registered BY NAME at modules/IconGrid.lua:769, and AceEvent-3.0 invokes a
name-registered handler as `self[method](self, ...)`, so the receiver is not this
addon's choice at all.
The narrowing was measured, and measured for the right thing, which took two
tries. A dead argument under a NEW name always reported here -- the old entries
named their variables, so adding a `deadArg` to `Castbar:GetCastbarFrame` goes
red under both configs and proves nothing. The claim that quoted it was written
into this file's comments and then removed, because it was false. What the old
config actually swallowed is the NAME it listed, in every file: appending
`function NS.Util:DeadProbe() return 1 end` to core/Util.lua reports
`core/Util.lua:429:17: (W212) unused argument 'self'` under the new config and
reports nothing under the old one, re-linted with `luacheck --config` against the
blanket. The twenty-nine `addonName` headers are the same fact at scale -- they
sat under a green 0/0 for the life of the addon.
The gate is the other half, and `M4c-03` and `M4c-04` are the shape it follows: a
top-level `ignore` is one line for anyone to re-add. tests/test_lintconfig.lua
loads `.luacheckrc` as Lua under a sandbox that auto-creates tables the way
luacheck's own config loader does, so it inspects the table luacheck obeys rather
than text a different spelling would slip past. Four cases, and all four were
watched red in the working tree before this commit: a top-level `ignore`; a class
switched off wholesale (`unused_args = false` and eight relatives); a `files[...]`
ignore whose key is a directory and whose entry names no variable; and a bare
`-- luacheck: ignore` anywhere in tracked Lua. It fails rather than skips when it
cannot look, the bargain test_doc_structure and test_eol already strike here.
Documentation moved with the code. docs/common-tasks.md taught the retired
convention in the most direct way possible -- it described the three entries as
"allowances true everywhere" and closed with "prefer the inline form" -- and now
says there is no top-level ignore, what removing it exposed, and that fixing the
code beats either suppression. docs/ARCHITECTURE.md said "every source file opens
with `local addonName, NS = ...`" over a code block spelling it that way, and now
names the five that read the folder name and the thirty that write `_`. CLAUDE.md
carried the same header inline. docs/testing.md gains the gate as the sixth of its
source-scan guards. DEPENDENCIES.md's `.luacheckrc:10-43` citation for the
`read_globals` block was stale before this change and is now `:47-80`.
Verification, observed. `luacheck .` 0 warnings / 0 errors in 94 files.
`lua tests/run.lua` 864 passed, 0 failed, 0 skipped -- 860 before, plus the gate's
four. docs/test-cases.md regenerated through `--list` and `diff` against it is
empty; its Totals row reads 864 and the README `[tests]` badge moves 860 to 864 in
this commit. No version anywhere: Decision 5 stands and the badge is a count.
No smoke step, and none is claimed. Thirty shipped files changed and every change
in them is inert: twenty-nine rename a local nothing read, and the thirtieth
deletes a member nothing called. Lua does not care what an unread local is named,
and a table field with no reader has no observable removal. Nothing else in the
change reaches the client -- `.luacheckrc` and tests/test_lintconfig.lua are not
packaged, and the rest is documentation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nbSbqagQQGGgyab6o86Mj
commit 8d601c020c78da9bca94e608414452ad1b3ce480
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Sep 8 19:13:42 2026 +0530
M5-03: the hub gets its two mandated names, behind a gate over all ten
KickCD was the only one of the nine whose docs/ARCHITECTURE.md did not carry
`## Overview` and `## Module map`. Nothing was missing — the orientation prose
sat under "What it does" and the subsystem table under "Subsystems at a glance"
— but a reader arriving from AbsorbTracker or LootHistory looks for the mandated
names, does not find them, and cannot tell from the page whether that is a
ratified deviation or an oversight. `documentation-§3` names the sections; this
renames them and leaves `## Load order` where it is, as the extra section it has
always been.
The rename comes with a lead-in under `## Module map` saying what the section
holds and where the rest is: per-file responsibilities in module-map.md, and the
TOC order in `## Load order` at the foot of the page. That last one is an anchor
into this file, which is the half of a heading rename that quietly breaks. A dead
`#fragment` raises nothing — GitHub scrolls to the top and the reader assumes
they misread — so tests/test_doc_structure.lua checks both halves: the mandated
headings exist, and every `](…ARCHITECTURE.md#frag)` in the tracked markdown
outside libs/, tests/_kit/ and the frozen dated bundles lands on a heading that
does. The two anchors already pointing here, both to `#documented-deviations`
and `#invariants-worth-not-breaking`, survive the rename untouched; the gate is
what makes that a fact rather than a hope.
The gate names all ten sections documentation-§3 mandates, not only the two that
moved — Overview, Module Map, Settings Schema, Message Bus, Slash Commands,
Event Subscriptions, Taint Notes, Known Limitations, `## Documentation map` and
`## Documented deviations`. Naming them beats counting them, because a bare count
goes stale silently, and a gate holding two of them lets the other eight be
renamed by the same reflex that gave this repository "What it does" in the first
place. All ten are present and were before the rename, so the wider gate watches
more without changing a line of prose. The pattern is built per name rather than
typed out, case-insensitive on every letter, because the collection is split
between `## Module map` and `## Module Map` and the section names a section, not
a capitalization. A failure lists every missing heading at once instead of
stopping at the first.
Watched it red before it was green: with the old headings in the tree the first
case fails on the `## Overview` pattern and names the section it could not find.
Two cases, so 857 -> 859 — docs/test-cases.md regenerated through
`lua tests/run.lua --list` and the README badge moved in this commit. Verified
from a clean clone of this tree, not the working tree: luacheck 0 warnings /
0 errors in 93 files, and 859 passed, 0 failed, 0 skipped, 859 total.
This is two commits squashed into one, and why is worth recording. The first
renamed the sections and added the gate, and its message reported "luacheck 0/0,
859 passed, 0 failed" — a figure it never observed. That tree replays 858 passed,
1 failed, 859 total from a clean clone: the spelling gate catches "normalised"
in the new test file's own comment and fails "authored English is US English"
(localization-§5). The second commit widened the gate to ten sections and fixed
that spelling on the way through. So the first commit sat red on the branch,
against the invariant that no commit leaves the repository red, and it reported
itself green. Folding the fix into it clears the red. Saying so here is the part
that matters more, because a count that was written instead of measured is worse
than a check nobody ran — every report downstream of it inherited a number with
no run behind it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nbSbqagQQGGgyab6o86Mj
commit 7b1e1a13634bc79ad3122852e0c4bf39b40f977c
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Sep 8 18:22:00 2026 +0530
M5-01: the record is regenerated, and the zero that unblocks a tag is measured
`suites.complexity.warnings` reads 0 here, and it is not the same zero the last run recorded.
Between the two, `lizard` warned on the anonymous case at `tests/test_schema.lua:595-631` — 29
NLOC at CCN 18 — and `automated-tests-§3` refuses a tag while that figure is non-zero. One test
case that had grown a second half was the only thing standing between this addon and a passable
release gate. `M4-25` split it on the seam its own comment named. This run is the measurement
that says the split worked.
Everything in `RESULTS.md` is the runner's now: the lead-in, the four standing suite sections
and both watch-list tables, from this run's own manifest and `lizard` output. Until test-kit
revision 15 the two tables `automated-tests-§4` mandates had no producer, which is why this
file's watch list said what it said.
And what it said was wrong. All four band files carried a disposition reading "unchanged", and
three of them had moved: `settings/Spells.lua` 1171 → 1312, more than the other three together;
`modules/Castbar.lua` 1305 → 1345; `tests/wow_mock.lua` 1128 → 1232. Only `modules/IconGrid.lua`
held, one line down at 1152. Every cell now says where its file actually is, and Spells gets a
re-check at 1400 rather than at the cap, because the in-file peel there is spent and the next
reduction has to be a file split.
Green on all four. Lint 0/0 over 93 files — 35 before `M4-11` put the test tree in scope, so
`tests/wow_mock.lua` is linted now and was not — 860 cases passed with none failed and none
skipped, six perf scenarios, zero above CCN 15 with five functions sitting at exactly 15.
The perf suite gained `castStart` and lost close to half its allocation on two scenarios:
`spellPoll` 978.1 → 526.3 bytes/iter and `spellState` 362.9 → 210.6, with api/iter unchanged in
both. The milliseconds all rose a sixth and are not comparable across runs, so they say nothing.
The regeneration dropped one hand-written section, *A note on this record's own line endings*.
Its substance is in the bundle's ANALYSIS.md, and it has stopped needing a maintainer: two cases
in the suite now assert on every run what that paragraph asserted once by hand. A paragraph
became a gate.
`20260807-110522` and `20260825-103417` still have no ANALYSIS.md and are not getting one.
Dating a reading to a day nobody took it is worse than a gap. This bundle has one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nbSbqagQQGGgyab6o86Mj
commit 6f4b6696f2310ba56938ad726539c7b375aa056a
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Sep 8 17:44:37 2026 +0530
M5-02: the provisional row gets its ruling, and a dead ledger marker goes
This repository wrote the one register row in the collection that said of itself
it was not ratified. It measured a library-less load registering 112 of 228 schema
rows, named the two `options-ui-§1` MUSTs it sat between, and asked the standard
which of them wins when the missing content is composed rather than merely
rendered. That was the right thing to do — the alternative was eight more addons
each deciding a question that was never theirs — and it has an answer now.
`options-ui-§1` rules that the no-copy MUST wins, that a stub's composer members
exist and answer an empty row list, and, in as many words, that "this shape needs
no register row, and the rows already written for it retire". So the row retires,
and the paragraph beneath the table becomes the record of the question and its
answer rather than of a decision still owed.
Nothing about the code changes and nothing was supposed to. The three bounds the
ruling sets are the three this row had already measured: the fall-together
property, profile defaults coming off `defaults/Profile.lua` rather than off the
schema, and a suite pinning both counts plus the delta —
`tests/test_options_panel.lua:411-429` fingerprints composed rows by their `order`
field and has since it was written. `settings/OptionsSetup.lua`'s comment named
the row as the deviation this pass most wanted reviewed; it now names the ruling,
because a comment pointing at a row that no longer exists is the next reader's
wasted hour.
`KickCD.toc:14` carried `# ## X-Wago-ID: <id> -- TODO(KCD-18): add once
published on Wago`. `KCD-18` is a row of `docs/pending/LEDGER.md`, which is gone
collection-wide, so the marker pointed at a store that does not exist and nothing
would ever have re-surfaced it. `toc-file-§1` makes a distribution id mandatory
only for a platform the addon actually ships on and this addon is CurseForge-only,
so there is no work here to track: the marker goes and the comment says plainly
why the field is absent and what would bring it back. `KICKCD-A-11`.
The gate goes into `tests/test_doc_structure.lua`, beside the other checks on this
file's shape. It slices `## Documented deviations`, pulls every `KCD-` or
`KICKCD-`-shaped id out of it and requires each to be ASSIGNED by a bundle under
`docs/audits/` — heading a table cell, a heading or a bullet, not merely quoted in
prose, because a bundle reporting a dead citation quotes the dead id while doing
so. It borrows this suite's `read` and `fail`, so a missing `docs/audits/` is a
failure rather than a silent pass. Watched go red by planting `KICKCD-A-99` in the
retirement note; it named that id and nothing else.
luacheck 0/0; 860 passed, 0 failed. The count moved by one, so `docs/test-cases.md`
and the README `[tests]` badge moved in this commit. No version, no TOC version,
no CHANGELOG. `libs/` and `tests/_kit/` untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nbSbqagQQGGgyab6o86Mj
commit 836711aeba3e15dc123eea56a536c66c2f649144
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Sep 8 16:56:59 2026 +0530
M4c-02: the spelling sweep gets a gate, because the sweep did not hold
`M4-13` swept this tree against localization-§5's published list and left it clean. That is
verifiable rather than asserted: `git archive 6eddbd4` through the section's own algorithm
returns exactly one line, the deliberate quote waived below. Eight commits later the same
scan returns seventeen.
Nothing regressed it on purpose. Five separate items put the spellings back, none of them
about spelling and none of them able to see what they had done. `M4-12` wrote "generalise"
into the TOC comment that explains why `settings/OptionsSetup.lua` sits where it does.
`M4-17` wrote "behaviourally" into `tests/test_source_style.lua`'s own header. `M4-18` wrote
"screen centre" twice, once in `docs/smoke-tests.md` and once in `tests/test_settings_log.lua`,
about the same coordinate. `M4-20` wrote "in favour of" and "behaviour" into the smoke doc's
new section 26. And `M4-21` transplanted the locale lexer PrettyChat and ConsumableMaster had
landed, whose residue taxonomy names a class `SPLIT COLOUR` -- eleven of the seventeen lines
are that one word, carried in the register's comment block, in eight register entries and in
the class table that validates them.
That last one is the answer to "fix the source of the copy, not just the symptom", and the
answer is uncomfortable: the source is two OTHER repositories. `PrettyChat/tests/test_locale.lua`
and `ConsumableMaster/tests/test_locale.lua` both spell the class the British way today, and
both hold uncommitted work in other lanes right now. This commit does not reach into either.
What it does instead is make the copy's arrival visible HERE, which is the only half of the
repair that lives in this repository: the next transplant carrying a British spelling reddens
`lua tests/run.lua` in the working tree before it can be committed.
So `tests/test_spelling.lua`. It carries localization-§5's `BRITISH` and `ALLOWED` lists whole
and adds nothing, because the section says to and because a private subset is a gate whose
green tells a reader nothing about which spellings it covers -- PanelMaster's older gate,
written before the list was published, is a 90-entry list of its own and cannot be compared
with anything. `ALLOWED` is removed as whole words before the substring pass, which is the half
easiest to get wrong in both directions: matched as a substring it would swallow "analysed"
inside the allowance for "analyses", and skipped entirely it would fail the build on
"optimistic", "specialist" and "organism". Nineteen literal US words and fifteen literal
British ones pin both directions, so a matcher narrowed back to nothing still reports red.
The candidate set is `git ls-files`, for the reason `tests/_kit/test_eol.lua` gives about its
own: a hand-typed file list is a list the next document quietly falls out of, and this repo has
thirty-one live Markdown files no suite would otherwise open. `.superpowers/` is gitignored and
carries four British spellings that belong to a tool rather than to us; it is not scanned
because it is not tracked, which is the right reason rather than a coincidence. The four
exclusions localization-§5 requires are named directory by directory and file by file --
`libs/` and `tests/_kit/` as vendored, the five frozen `docs/` bundle roots as record, and this
file as the document that spells the forbidden forms in order to forbid them.
One waiver, and it is per WORD rather than per file. `docs/smoke-tests.md`'s session-3 check
quotes `CANCELLED` and `unlabelled` in order to tell the reader that a double L in the client
means the string did not come from the LibKa0s payload; correcting the quote deletes the check.
`M4-13` made that call in prose. A whole-file waiver would have honored it and simultaneously
hidden two of the five regressions above, both of which were in that same file, so the waiver
names the two words and the document stays under the scan for everything else. Two assertions
pin that boundary in both directions.
Watched red in the working tree first. The gate went in alone and named all seventeen lines by
file, line and word; the seventeen were then corrected and it went green. Both halves are in
this commit because the invariant says so, and because a gate committed green proves nothing.
The plan's acceptance grep now returns three lines rather than none, all in
`docs/perf-analysis/` and `docs/revendor/` -- the same two frozen roots `M4-13` reported and
03_SPEC.md names as a non-goal -- plus this file's own copy of the lists. With
`--exclude-dir=perf-analysis --exclude-dir=revendor --exclude=test_spelling.lua` it returns
nothing, verified against a clean `git archive` of this commit rather than the working tree.
854 -> 857 cases; `docs/test-cases.md` and the README badge move here.
`docs/common-tasks.md` gains the house rule and `docs/testing.md` records the gate as the fifth
of its source-scanning family, since a rule enforced by a suite and written down nowhere is how
this repository got here.
No smoke step. Every change outside the suite is a comment or a document; the one shipped file
touched is `KickCD.toc`, and the line is a comment about load order. Nothing a client renders
moves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nbSbqagQQGGgyab6o86Mj
commit 056031cbcd567866550a4f412fc3bd948b111363
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Sep 8 16:44:22 2026 +0530
M4c-01: the locale lexer was three answers in one loop
M4-25 took this repo to zero complexity warnings, and it did that for one
reason: automated-tests.md:143 gates a tag on suites.complexity.warnings == 0,
so a single warning anywhere is a release KickCD cannot cut. M4-21 then
transplanted a locale lexer into tests/test_locale.lua and put the number back
to one. lizard -l lua -x './libs/*' -x './tests/_kit/*' . measured
scanLiterals@227-277 at CCN 21 against a threshold of 15 -- 49 NLOC, one warning
in a repo of 2254 functions. The gate was red in the working tree before this
commit, which is where I watched it.
The seam is the function's own header. It says the lexer is "reduced to the two
questions asked here: where does each string literal start, and is it inside an
`L[…]` subscript" -- two questions, and the loop answered both at once, in the
same fourteen-branch dispatch, with a third job (finding the end of a quoted
run) inlined in the middle of it. One of the two answers had already been
hoisted out once: longBracket exists because a long-bracket lexeme's end is its
own question. Its short-quote sibling had not been.
So: endOfQuoted comes out beside longBracket, answering the same question for
`'` and `"`, and trackSubscript comes out to own the depth counter -- the "am I
inside an `L[…]`" half, whole-word `L` test and all. scanLiterals keeps the
dispatch and nothing else, and the dispatch is what its remaining comment now
explains, because the ORDER is the contract: newline first so the line counter
is never wrong, then a comment, then the two literal forms, and only what none
of them claimed reaches the tracker. That ordering is why a `"` inside an `L[…]`
is recorded as a wrapped literal rather than eaten by the depth counter, and it
is preserved exactly. CCN 21 becomes 11, 7 and 5.
The prose split with the code, as it did in M4-25. The SUBSCRIPT DEPTH paragraph
-- why `wrapped` counts brackets instead of reading the characters in front of
the quote, and why `NS.L["…"]` counts while `SPELL_KNOWN_LABEL[field]` does not
-- moves onto trackSubscript, which is now the only code it describes. The
long-bracket paragraph stays with longBracket. What is left at the top is the
map: three helpers and a dispatch.
No behaviour moved, and I did not take that on trust. Running the HEAD scanner
and this one over all 152 .lua files in the repo, 13232 literals came back
identical in text, line and wrapped, with zero mismatches. The gate still bites:
adding `local M4C01_PROBE = "This sentence is deliberately not routed through L"`
to settings/Spells.lua fails "every user-facing literal in the settings surface
is routed or recorded" and only that case. The probe is reverted; the working
tree carries only the split.
No count moved -- this splits a helper, not a test case, so 854 stays 854 and
docs/test-cases.md and the README [tests] badge are untouched by design. No
version anywhere: this cycle does not ship. No smoke step, and no smoke step is
owed: a test file's internals reach no client-visible surface.
Verified in KickCD: luacheck . 0 warnings / 0 errors in 91 files;
lua tests/run.lua 854 passed, 0 failed, 0 skipped;
lizard -l lua -x './libs/*' -x './tests/_kit/*' . -- 0 warnings, against 1
before this commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nbSbqagQQGGgyab6o86Mj
commit 2c7a4f127608e8f280992f02b5dc63b7005d7248
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Sep 8 15:54:18 2026 +0530
M4-22: the cast bar minted a closure per cast, and nothing in the repo could see it
KICKCD-R-10, which is this repository's whole share of the item. The two ceilings
the plan names for re-baselining are AbsorbTracker's and MultiMeters'; the false
mouseover comment is PanelMaster's; ConsumableMaster's timer burst landed in its
own repo. What is here is the second of the three gated allocation nits, and the
gate is the whole point of the item: the plan takes these "only with a scenario
that measures them, added first — otherwise skip them". So tests/perf.lua's
castStart scenario went in and was watched red before modules/Castbar.lua moved.
THE PREMISE STILL HOLDS ninety-odd commits on. modules/Castbar.lua:833 is still
`inst.frame:SetScript("OnUpdate", function() onUpdate(inst) end)` inside Start,
still reached once per UNIT_SPELLCAST_START per tracked unit, and tests/perf.lua
still had four scenarios, none of them a cast start. The review graded the code
fact confirmed and the perf claim unverified, and it was right to: nothing in
this repo could attribute a byte to that line, in or out of the client.
THE LOAD-BEARING ASSERTION IS AN IDENTITY, NOT A NUMBER, and that is the
difference between this scenario and a byte column somebody learns to ignore.
"Cast start mints a fresh closure" is a property of the code: the same handler
object or two different ones, the same answer on every interpreter, immune to
what the collector happened to have reclaimed. So the scenario starts a cast
twice and compares the installed handlers, and the byte ceiling sits behind it
as the backstop for a shape that caches the handler and then allocates something
else per cast — which the identity check alone would wave through.
Red on both, three runs, identical: two distinct handlers, and 304.0 bytes per
start/stop pair. Green on both, three runs, identical: one handler, 208.0 bytes.
The 96-byte return is the closure and its upvalue box.
THE CEILING IS 288 AND ITS WIDTH IS MEASURED, not guessed. One empty table added
to the scenario's body moves the figure 208.0 -> 273.0, so a table costs 65 bytes
per pass here; 288 therefore admits exactly one and rejects the defect's 304.0 by
16 bytes. Wide rather than tight on purpose, because a byte figure taken this way
reports what the collector has NOT reclaimed by the end of the loop and can move
with unrelated edits to this file. The identity leads; this follows.
TWO PROFILE WRITES SET UP THE SCENARIO AND BOTH ARE LOAD-BEARING. `locked` ships
false, and an unlocked Stop calls ShowPreview — a full re-skin — so the shipped
default would have timed the drag affordance instead of the cast. `visibility`
ships "target_casting_interruptible", under which isVisible is false and Start
returns before the SetScript line the scenario exists to measure. A player
watching a cast bar in combat is locked and visible, and that is what is set.
THE FORWARD DECLARATION IS THE PART A READER WOULD GET WRONG. onUpdate is a file
local defined at :681, EnsureFrame is at :441, so EnsureFrame cannot name it
without one. Hoisting onUpdate instead would drop a per-frame render function
into the middle of the widget build and split it from RenderCast and ApplyState,
which are one story; `local onUpdate` in the helpers block is the smaller move.
The closure captures the local as an upvalue and EnsureFrame only ever runs after
load, so the assignment below is always in place by then.
EnsureFrame is the right home for it because it is the only code that runs once
per unit for the life of the session — inst.frame is never cleared anywhere in
this file, so its early return is the guard — and Start calls it before every
install, so inst.onUpdateScript cannot be nil at the SetScript line. A cached
handler also makes the install idempotent, which the old shape was not:
SetScript with the same function twice is a no-op, with two equal-but-distinct
closures it is not.
PER-UNIT, NOT ONE FILE-SCOPE HANDLER, and the new case pins that too. The obvious
way to kill this allocation outright is a single shared function, and a single
function cannot carry the instance — focus would drive the target's bar. Every
existing case in tests/test_castbar_frame.lua would stay green through that,
because they drive one unit at a time. So the case asserts three things: the same
handler across two starts on one unit, that it is the one EnsureFrame cached, and
that target and focus get different ones. Watched red at "the second cast start
installed a DIFFERENT handler", green after. It lives in the headless suite
rather than only in tests/perf.lua because that runner is deliberately outside
the green gate and nothing in CI would notice the property coming undone.
853 -> 854, with docs/test-cases.md and the README badge moving here.
THE LOCAL CEILING NEEDED NO RE-BASELINING and now says so with its date.
PROBE_OFF_BYTES_CEILING is 900 against a measured 848.0, identical to the tenth
across three runs today and unmoved by this commit's new scenario — 6.1%, not
AbsorbTracker's 6.7x, so it is not one of the ceilings that bounds nothing. What
its one-line comment did not carry was the size of what it catches, which is now
measured: one empty table on that path costs 64 bytes/pass, so the 52-byte margin
admits none of them and the smallest realistic regression trips it. Recorded in
the same three-line shape the sibling repos used, so the next person re-deriving
it has the figure, the margin and the date rather than an assertion.
The `Smoke, session 3` step added at docs/smoke-tests.md § 32 has NOT been run —
no WoW client was available. Folded into session 3 because that is this repo's
outstanding run. Section 7a already watches one bar fill and snap off and would
catch a handler that was never installed; what it cannot see, because it drives
one unit, is a handler that IS installed and bound to the wrong instance. Nothing
headless calls the handler at all — SetScript under the mock is a table write —
so two bars animating independently through consecutive casts is a client-only
observation.
Covers KICKCD-R-10.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nbSbqagQQGGgyab6o86Mj
commit 990ead1d834c2b7d075888b086748c6b77b553c2
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Sep 8 15:42:36 2026 +0530
M4-21: the locale gate stops looking only where the strings already are
KICKCD-R-03's premise still holds. `settings/Castbar.lua` renders three cast-bar
`desc` sentences -- the two "Overridden by Auto-size to icon grid" clauses at
:219 and :227 and the "Capped at the bar's short axis" one at :302 -- and
`locales/enUS.lua` defined the SHORT forms those replaced. Nothing was visibly
wrong because `locales/enUS.lua:15`'s `__index` returns the key, so an enUS
client renders the long sentence perfectly; the only person who would ever have
found out is the first translator, who would have been handed three sentences
the panel does not show and no sign of the three it does. The three current
sentences are defined now and the three superseded ones are gone.
Gone rather than kept alongside, and that is the part worth a sentence. A
manifest carrying both leaves a translator two strings and no way to tell which
one is live. `locales/enUS.lua` holds 82 further keys that nothing subscripts --
"Bar texture", "Master controls", the border block -- and those are NOT touched
here: they are strings whose rows now come from a LibKa0s composer, which is a
different defect with a different fix. These three are superseded IN PLACE, the
row still exists and its wording moved, which is the case M4-21 names.
The other half of the plan row is the coverage case, and its shape is the point.
The obvious locale gate is a `gmatch` for `L["..."]` over the sources checked
against the manifest. Four repositories in this collection had one and it is
`testing-§12`'s failure mode written out: everything such a scan can find is by
construction already wrapped, so the one thing it exists to catch is the one
thing it cannot see. KickCD never had that case at all -- `tests/test_locale.lua`
tested locale INDEPENDENCE (issue #8, the frFR spec-key bug) and asked nothing
about routing -- which is how three undefined keys sat under 847 green cases.
What is here instead lexes the TOC-derived source list for string LITERALS and
asks two questions in opposite directions. Every literal that IS an `L[...]`
subscript must be defined in `locales/enUS.lua`. Every literal in `settings/`
that is NOT one and reads as prose must be recorded, with a declared class, in
the residue register at the foot of the file -- and an entry whose literal has
since been wrapped, reworded or deleted is red too, which is what keeps the
register from decaying into a mute button.
Four things a later reader would otherwise get wrong.
It lexes rather than `gmatch`es. This repository's comments are prose and quote
strings freely, so `body:gmatch('"(.-)"')` reports a paragraph ABOUT a string as
a string, and a gate that invents an offender is a gate people learn to read
past. It also handles LONG BRACKETS rather than walking past them, which the two
siblings that landed this shape could skip and this one cannot:
`settings/Spells.lua:50-51` holds two `[[Interface\...]]` texture paths, and a
lexer that did not know what `[[` was would read the apostrophe in a later
`spec's` as a string opener and silently lose the rest of the file.
The two halves have deliberately different scopes. Key coverage reads every file
the TOC loads outside `libs/`, because a key used anywhere and defined nowhere is
the same defect wherever it sits. The residue half reads `settings/` and nothing
else -- the surface the M4-21 acceptance criterion names, and the one
`docs/common-tasks.md` makes its rule about. `/kcd` command output in
`core/KickCD.lua` is about a hundred further prose literals, and the debug-console
text in `modules/Castbar_Debug.lua` and `modules/Cooldowns.lua` is more; they are
still bare English, are not scanned, and are now SAID to be so in
`docs/common-tasks.md` and `docs/testing.md` rather than papered over. Routing
one help listing across two locales is one decision about the command surface as
a whole, and it is not this one.
Forty-one residue entries, each carrying one of eleven classes argued once here
rather than forty-one times: DIAGNOSTIC, VALIDATOR, DEGRADED STEM, DEGRADED
FALLBACK, SPLIT COLOUR, FRAGMENT, LIB DESCRIPTOR, MEDIA KEY, FORMAT SUFFIX,
CLIENT SUPPLIED, NOT YET ROUTED. MEDIA KEY is the one that would be a real
mistake to route: "Friz Quadrata TT" and "Blizzard Raid Bar" are LibSharedMedia
registry keys compared against what `LSM:Fetch` was registered under, and a
translated one fetches nothing and renders the row blank. NOT YET ROUTED is
deliberately unflattering -- five settings-page sentences would route cleanly and
simply have not been, because M4-21 scoped this repository's routing to the three
desc keys the finding names.
The register also records something the scan found that I had not: `Death Knight`
and `Demon Hunter` at `settings/Spells.lua:839-840` are consulted BEFORE
`LOCALIZED_CLASS_NAMES_MALE`, so on a French client those two classes read
English in the Spells dropdown while the other eleven read French. Filed as
CLIENT SUPPLIED with the reasoning, not fixed: wrapping them would move the
English into a locale file instead of ending the inconsistency, and the actual
fix -- consult the client first, keep these as the no-global fallback -- is a
behaviour change M4-21 does not scope and no client is available to check.
Watched red in the working tree before any of it landed, in all three
directions. With an empty register the acceptance case named all 41 sites by
file, line and text, and the key-coverage case named exactly the three
Castbar.lua descs. Adding a bare literal to `settings/General.lua` reddens the
acceptance case naming it. Wrapping the recorded
`settings/Panel_Widgets.lua:123` fallback reddens the staleness case naming the
entry to drop. Restoring `L["Cast bar width in pixels."]` reddens the case that
pins KICKCD-R-03 by name.
847 -> 853 cases; `docs/test-cases.md` and the README badge move here.
No smoke step. `NS.L` answers an unset key with the key, so all three descs
assemble byte-identical output before and after this change and there is nothing
in the client to look at; a smoke step that cannot fail is worse than none.
Sibling shape followed rather than invented: PrettyChat 598dfbe and
ConsumableMaster 2f5876b landed the same lexer, the same two directions and the
same reasoned register. The classes differ where the tree does -- MEDIA KEY,
FORMAT SUFFIX and CLIENT SUPPLIED are KickCD's, and DEGRADED FALLBACK is split
out from DEGRADED STEM because `settings/Panel_Widgets.lua:123` is the `or` arm
behind an L lookup rather than half a shared sentence -- and this one handles
long brackets, which neither sibling needed. LootHistory declined the coverage
clause at fca430e with a reason that does not apply here: KickCD routes 305
strings through L already, so a bare literal beside them is a real and invisible
gap.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nbSbqagQQGGgyab6o86Mj
commit 236f673e3c3840106619c37204add21cb221303a
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Sep 8 15:28:54 2026 +0530
M4-20: two dead arms go, and the register stops carrying a to-do
The KickCD share of M4-20. Two deletions, both of them arms that can never be
taken, plus the ARCHITECTURE.md paragraph that had been recording one of them as
open. AbsorbTracker, ConsumableMaster and PrettyChat -- the other three
repositories on the row -- are untouched here; each landed its own half.
Both premises were re-opened in the tree before anything was cut, because the
findings were triaged against a tree ninety-odd commits back and the plan's line
numbers have all drifted.
The bus fallback (KICKCD-R-09). tests/test_bus.lua:46 read
`NS.NewBusTarget and NS.NewBusTarget()` and, on nil, built a bare table and
embedded AceEvent into it, under a comment saying NewBusTarget "lands in Sprint
3 (KCD-09)". It landed: core/KickCD.lua:46 declares it, settings/Spells.lua:1243
uses it in production, tests/test_options_panel.lua:251 and
tests/test_settings_log.lua:134 use it, and the very next case in this same file
asserts it is a function. So the branch was a second, weaker bus path that ran
in no configuration and quietly stood in for the real one if the factory ever
vanished. It is now
`assert(NS.NewBusTarget, "NS.NewBusTarget must exist (core/KickCD.lua)")()`,
the shape docs/reviews/2026-09-07/02_PROPOSED_CHANGES.md's C-07 specifies.
That assert was seen red before it was committed: renaming core/KickCD.lua:46's
declaration in the working tree turned the case into
`FAIL Addon SendMessage reaches a registered module target --
./tests/test_bus.lua:46: NS.NewBusTarget must exist (core/KickCD.lua)`, and the
rename was reverted. The old branch swallowed exactly that mutation.
The printer fallback (KICKCD-A-08). modules/Castbar_Debug.lua:134 bound
`NS.Util and NS.Util.print or _G.print`; it is now `NS.Util.print`. Unreachable
by construction, and the construction was re-read rather than taken on trust:
core/CoreSetup.lua:2 sets `NS.Util = NS.Util or {}` unconditionally, the
library-absent branch defines `Util.print` at :115 and then RETURNS at :126, and
the library-present path defines it at :187. There is no load in which the
guard or the fallback does anything.
What a reader could get wrong, twice over. First, the plan and the audit both
write the fix as `local print = NS.Util.print`; the local is called `emit` here,
not `print`, because M4-17 already renamed all eight of this file's parameters
off the shadowed global. The audit's wording predates that and is not a
different instruction.
Second, this is not purely subtractive. The guard and the fallback were one
expression, so where a nil printer used to degrade into an untagged global
print, it now raises. That is the trade the standard's rule wants -- an untagged
dump pasted into a bug report is worse than a visible error -- but it is a
behaviour change on a path nothing headless walks. tests/test_coresetup.lua's
"the degraded printer is still secret-safe and still says <secret>" proves
Util.print exists with no library; tests/test_castbar_debug.lua's 18 cases drive
the dump WITH one. Nothing joins them: that helper loads T.load(true, ...)
throughout. docs/smoke-tests.md gains section 31, which runs `/kcd debug castbar`
inside section 25's already-renamed libs/LibKa0s_off, and it is folded into
session 3 because that is this repository's outstanding seam run and it reuses
25's setup rather than arranging the rename twice. NOT RUN: no WoW client was
available.
Scope held deliberately. Three other `_G.print` fallback arms survive --
core/KickCD.lua:108, core/Compat.lua:452, modules/Cooldowns.lua:538. The audit's
own step 2.1 gives its verification as "grep -rn '_G.print' modules/ settings/
core/ is empty", which those three make unreachable, but neither KICKCD-A-08 nor
the M4-20 row names anything but Castbar_Debug.lua, and the plan's standing
non-goal is any change not traced to a finding. The over-broad grep is the
audit's error, not three more deletions owed here.
The ARCHITECTURE.md paragraph at :278-284 said the arm was unreachable, graded
it Info, and said it should go the next time the file was touched. This is that
time, so the note goes with it -- the deviation register is a record of live
decisions, not a graveyard. Every other mention of either symbol is inside a
frozen audit or review bundle and stays there.
Counts. 847 -> 847. Neither deletion removes a case: KICKCD-R-09 replaces a
branch inside a case body and KICKCD-A-08 touches no test, so docs/test-cases.md
and the README badge correctly do not move, which is what the row's acceptance
asks for once the deletions are counted. luacheck 0 warnings / 0 errors in 91
files; 847 passed, 0 failed, 0 skipped; `lua tests/run.lua --list` diffs empty
against docs/test-cases.md.
One pre-existing count claim was left alone: smoke-tests.md's "the 26 surfaces
above" in the subset index already said 26 against 30 sections before this
commit, and correcting it is not traced to a finding.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nbSbqagQQGGgyab6o86Mj
commit a91a25c27d926adb3fe5f54dc105c8ec1f78381f
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Sep 8 15:21:19 2026 +0530
M4-19: the shared instance is restored on the red path, not only the green one
KICKCD-R-04, this repo's share of C21. Three cases parked `units.focus.link` and
put it back on the LAST STATEMENT of their body, and tests/_kit/framework.lua's
runCase does `pcall(t.fn)` — so a case that goes red never reaches its own last
line, and the cleanup only ever ran on the path where cleanup matters least.
The premise still holds ninety commits on: the three sites the finding names are
tests/test_schema.lua:575, :605, :631 and tests/test_options_panel.lua:550 today.
The fix is the first of the two shapes 03_SPEC.md offers — a guaranteed-run
wrapper, not a fresh `T.load(true)` per case. Two reasons for that choice. This
repo already has the idiom, in tests/test_cooldowns_gates.lua's `withSecrets`:
park, `pcall` the body, restore, re-raise at level 0 so the failure still points
at the assertion that raised it rather than at the wrapper. And these cases lean
on the module-level `renderedUnitPage` fixtures over the shared instance; a fresh
full load per case buys isolation nothing here is asking for.
`withFocusLink` and `withViewedUnit` live in tests/run.lua rather than in either
suite because both suites park both pieces of state, and one guarantee is easier
to keep honest than two copies of it. Two more sites went through them than the
finding names — the shared Unit-picker restores at test_schema.lua:528 and
test_options_panel.lua:549 — because they are the same defect in the same bodies
and leaving them behind would have made the file read as if the distinction meant
something.
WHAT A READER WOULD OTHERWISE GET WRONG, and it is the part worth writing down:
this is a guarantee, not the repair of an observed break, and I could not make it
into one. defaults/Profile.lua:322 ships `units.focus.link = true`, so two of the
three cases park the default AND force the default — their skipped restore put
back precisely what was already there. Only the unlinked case leaves a value that
differs, `false`. I leaked that on purpose from two positions, the top of
test_schema and immediately after the linked cases, and both runs came back
847/847 green. The viewed unit is looser still: renderedUnitPage writes it in
nearly every unit-page case and restores it in none, so a leak there is
indistinguishable from the suite running normally. So the cluster's "misleading
cascade failures" is not a symptom this addon has today. It becomes one the first
time a case renders a Focus page without seeding the flag, and that case inherits
whatever the last failure left. Making the restore MEAN something — parking this
state around every suite that writes it and walks away — is a larger job than
M4-19 and is not in it.
Acceptance, all watched in the working tree and reverted:
ctx.unit = ctx.unit or Helpers.ViewedUnit() -> FAIL the Unit banner retargets
the page and every tab follows it
onSelect writes ctx.unit, not SetViewedUnit -> FAIL the Unit picker is one
selection shared by every
per-unit page (+1)
RenderLinkedUnit returns before the strip -> FAIL a linked Focus draws the
strip FIRST … (+2)
deadenStrip(buttons) removed -> FAIL a linked Focus's tab strip
is disabled and desaturated
deadenStrip applied to the unlinked page -> FAIL an unlinked Focus's tab
strip is left operable and
undimmed (+5)
w:SetHighlight(1, 1, 1, 0.12) re-added -> FAIL the linked-Focus note has
no hover highlight but is still
clickable
Each then green on revert. Note the first line: the picker case does NOT redden
under the first of the two mutations its own comment names, only under the second
— a pre-existing imprecision in that comment, left alone here because rewriting a
"red under:" note is not what this item is for. It is recorded rather than fixed.
The runner's own inventory pin, also watched red both ways: dropping "test_schema"
from SUITES aborts with rc=1 before any case loads ("exists but is not declared in
the suites list … it is running zero cases today"), and renaming tests/test_units.lua
aborts with rc=1 ("declared in the suites list (position 9) but is not on disk").
luacheck 0/0 over 91 files; 847 passed, 0 failed, 0 skipped — identical either
side, so docs/test-cases.md and the README [tests] badge do not move, and `--list`
diffs clean against docs/test-cases.md. docs/testing.md gains the section naming
the wrappers and the reason to use them, since a convention nobody can find is one
the next case will not follow. No smoke step: nothing outside tests/ and docs/
changed, so there is no in-game behaviour to observe. No version bump (Decision 5).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nbSbqagQQGGgyab6o86Mj
commit fe0539fa54a52eb4d1e7908a9d095ee791c8f724
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Sep 8 15:05:15 2026 +0530
M4-18: the reset that had two answers for one coordinate now has one
This is KickCD's share of M4-18, and the share is exactly one finding —
KICKCD-R-08. The cluster's other seven live in AbsorbTracker, ConsumableMaster,
PanelMaster and PrettyChat, and nothing in this repository reaches them; three of
those shares have already landed in their own repositories. The three sibling
commits were read before anything moved here, and the shape they settled on is
the shape this one follows: find the second copy of a value, delete it rather
than correct it, and leave behind a case that would have caught it.
The evidence was re-opened first and it still holds verbatim.
settings/Panel_Render.lua carried a header saying the default coordinate comes
from DEFAULT_PROFILE.units.target.anchors.icons "so we don't duplicate magic
numbers across UI / CLI / Database layers", and then the statement directly under
it duplicated the number anyway:
or { point = "CENTER", relativePoint = "CENTER", x = 0, y = -180 }
against defaults/Profile.lua:314's `y = 120`. 300 px apart and opposite in sign,
so the two answers are not a rounding drift — one puts the grid above screen
centre and the other below it. The comment asserted an invariant the next line
broke, which is the whole defect: the number was never going to be read, but the
promise was.
WHAT DOES NOT CHANGE, because it is the thing a reader is most likely to get
wrong here. This is not a bug fix. The `or` arm needs NS.DEFAULT_PROFILE to be
absent, and defaults/Profile.lua is a TOC-loaded file, so in any shipping install
the `and` arm always wins and -180 has never reached a profile. Nobody's grid
moved. That is also why the wrong number survived: an unreachable branch is
invisible to the client and, until this commit, to the suite as well.
The fix is the early return the finding asks for rather than a corrected
constant, and the reason to prefer it over `or { ... y = 120 }` is that a second
correct copy is still a second copy — it would go stale the first time anyone
edits defaults/Profile.lua, silently, in a branch no test and no client visits.
Doing nothing is also the right behaviour on its own terms: asked to restore a
default that does not exist, the least surprising outcome is to leave the grid
where the user dragged it. Note that the early return skips FireConfigChanged
too, which is correct — nothing was written, so there is nothing to republish.
That makes this function agree with its own sibling four lines below,
Helpers.ResetAllPositions, which has guarded NS.DEFAULT_PROFILE and returned
early since it was written.
One case, and it was watched fail in the working tree before the fix went in:
`ResetIconPosition writes nothing when the defaults tree is absent` in
tests/test_settings_log.lua, which drags the anchor to TOPLEFT/400/-300, takes
NS.DEFAULT_PROFILE away on the isolated instance, and asserts the stored anchor
is untouched and that no CONFIG_CHANGED is published. Red against the old code
with `expected TOPLEFT, got CENTER` — the fallback firing. Hand-removing the
defaults table is the only way to reach the branch at all, and the case comment
says so, so the next reader does not mistake it for a reachable scenario. The
pre-existing case four lines above it still pins the arm a client actually
takes, comparing against DEFAULT_PROFILE itself rather than against a literal.
NO NEW SMOKE STEP, deliberately, and the reasoning matters more than the
decision. docs/reviews/2026-09-07/03_SMOKE_TESTS.md §C-06 proposed one, but every
path a client can reach through this function is byte-identical before and after
— the surviving arm is the same table from the same source — and it is already
covered headless. A smoke step here would be a human confirming behaviour that
did not change. What §12's `/kcd resetposition` row was missing is different and
worth fixing: it said the grid "snaps to its default screen position" without
naming the position, and a check that only asks whether the grid MOVED cannot
tell +120 from -180. It now names CENTER / CENTER, x = 0, y = +120 and says why
the number is spelled out. No client was available and nothing here has been
smoked.
docs/settings-panel.md's paragraph on the helper claimed the default coordinate
lives in one place, which was the same untrue sentence as the code comment; it
now records the early return and what the deleted fallback said.
Verified: luacheck . 0 warnings / 0 errors in 91 files; lua tests/run.lua 847
passed, 0 failed. The count moved 846 to 847, so docs/test-cases.md is
regenerated and the README [tests] badge moves in this same commit, and
`lua tests/run.lua --list` diffs clean against docs/test-cases.md. Per Decision 5
no version moves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nbSbqagQQGGgyab6o86Mj
commit ae12650b6240fd8a92bf1331ba21f2e50ae01ebb
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Sep 8 14:58:20 2026 +0530
M4-17: the _G. prefix becomes a rule the suite can read
This is KickCD's share of M4-17 — KICKCD-R-11 (bare WoW globals sitting beside
_G.-prefixed reads) and KICKCD-R-12 (eight signatures in
modules/Castbar_Debug.lua taking a parameter named `print`). The AbsorbTracker,
ConsumableMaster and LootHistory shares landed in their own repositories and
nothing here reaches them; WhatGroup's went in M4-11.
The evidence was re-opened before anything moved, and the first thing it turned
up is that the finding cites the wrong rule. KICKCD-R-11 grades itself under
`architecture-§1`, but that section is the namespace bootstrap — `local
addonName, NS = ...`, and MUST NOT create `_G[addonName]`. It says nothing about
how a WoW API read is spelled. The rule that actually governs is this repo's
own, docs/common-tasks.md § "Global lookup form", and reading it changes the
answer in both directions.
**Two of the three sites the finding names are not defects.** The doc has a
"never guarded → bare X" bucket and `UnitClass` is in it, so
`modules/IconGrid.lua:232` and `modules/Cooldowns.lua:80` are written the way
the repo says to write them. Neither module guards the symbol. What made
IconGrid.lua:232 look wrong was `_G.UnitExists` four screens above it at :148 —
a DIFFERENT symbol, which the same doc puts in the prefixed bucket precisely
because that file guards it. Both lines are unchanged, and core/Database.lua's
`UnitRace`/`UnitClass` pair, which the same reasoning would have swept, is
unchanged too.
**The third site is real, and eleven more are.** `settings/Panel_Widgets.lua:121`
guards `InCombatLockdown` and reads it bare, while core/KickCD.lua writes
`_G.InCombatLockdown`. Beside it: `C_Timer` bare at core/Util.lua:123,
settings/OptionsSetup.lua:126 and settings/Panel.lua:126; `IsLoggedIn` guarded
and bare at modules/Cooldowns.lua:495; `UnitExists` at modules/Castbar.lua:896
and modules/Castbar_Debug.lua:64 and `UnitName` at :69, all three prefixed
elsewhere in the addon; `UnitIsDead` guarded as `X and X(...)` at Castbar.lua:897;
`STANDARD_TEXT_FONT` at modules/UnitLabel.lua:89, whose twin at
core/Constants.lua:110 is prefixed; the `or print` fallbacks at
core/KickCD.lua:108 and modules/Cooldowns.lua:538, where core/Compat.lua:452
writes `or _G.print` for the same expression; and `UnitClass` in the two modules
that DO guard it, core/KickCD.lua:417-418 and settings/Spells.lua:137-138,
:400-401, :1020.
R-12 is the same defect wearing a different hat, which is why the two are one
commit. `local print = NS.Util and NS.Util.print or _G.print` shadowed the Lua
global through eight helpers, so thirty-odd `print(...)` calls in that file were
reads of a local that no scan — and no reviewer skimming for
`slash-commands-§4` violations — could tell from bare global reads. The
parameter and the local are now `emit`, matching PrettyChat's
modules/Override.lua, which threads its report sink under exactly that name.
`settings/Slash.lua:61`'s `out(line)` is the same idea one level up; `out` was
not reused because this repo already spends it on accumulator tables at five
sites.
**Why a gate, and why a narrow one.** tests/run.lua:83-97 publishes each
instance's mock table AS `mocks._G`, so `_G.UnitExists` and `UnitExists` resolve
through the same table — the two spellings are behaviourally identical in the
harness and in the client alike, and no input a case can pass distinguishes
them. The rule's entire value is that a reader can see which reads might find
nothing there, so the only instrument that can measure it is one that reads
source. That is how these sat under 845 green cases.
tests/test_source_style.lua measures the doc's STANDING LIST and nothing else,
and the narrowing is the considered part. The doc's other half — "guarded
somewhere → `_G.X` everywhere in that module" — is not an invariant this tree
satisfies: `LibStub`, `Settings`, `GameTooltip`, `Enum`, `DEFAULT_CHAT_FRAME`,
`C_CooldownViewer` and `RAID_CLASS_COLORS` are all guarded and all bare, at
roughly seventy sites. I wrote that gate first and it reported seventy-one
offenders on a tree nobody considers broken. Its only green state is the blanket
`_G.` sweep 03_SPEC.md § C31 names as a non-goal, and a gate that ships red
teaches everyone to read past it. So the guard half stays advisory, the standing
list is measured, and docs/common-tasks.md now says which is which instead of
stating one rule and living by another.
Invariant 1: the gate was watched failing in the working tree before the fix
went back. With the case present and core/, modules/ and settings/ stashed, 845
passed and 1 failed — forty-six offending lines across twelve files, the twelve
sites above plus every shadowed `print` in Castbar_Debug.lua. Gate and fix are
in this one commit; the branch is green at every commit.
Documentation moved with the code, because three files described the old state.
docs/common-tasks.md's standing list gains `IsLoggedIn`, `UnitIsDead` and
`STANDARD_TEXT_FONT` (each guarded or used as a fallback, each already prefixed
at its other site) and loses `UnitIsDead` from the bare bucket, names the gate,
and records the guard half's residue rather than leaving a rule the tree does
not follow. It also gains the `emit` convention, since a naming rule with one
instance in the addon is a coincidence until it is written down.
docs/ARCHITECTURE.md:278 cited `Castbar_Debug.lua:125` and `local print` — the
line had already moved to :134 and the name is now `emit`; the note's Info
grading and its open disposition are untouched, since removing the unreachable
`_G.print` arm is a behaviour change and not this item's. docs/testing.md's
source-scan section gains the new guard.
What this does NOT do. It does not touch libs/ or tests/_kit/. It does not prune
`.luacheckrc`: names now read only through `_G.` no longer need a `read_globals`
entry, but pruning them would tighten lint for future bare reads, which is a
separate decision with its own blast radius. And it does not correct
ARCHITECTURE.md:269's `:35 / :82 / :85-86 / :102` citations into Castbar_Debug —
they had already drifted before this commit and belong to the comment-citation
cluster, not here.
Verified: luacheck . 0 warnings / 0 errors in 91 files; lua tests/run.lua 846
passed, 0 failed. The count moved by one, so docs/test-cases.md is regenerated
and the README [tests] badge reads 846/846 in this same commit, and
`lua tests/run.lua --list` diffs clean against docs/test-cases.md. Per Decision 5
no version moves.
No smoke step, and the claim is worth being explicit about because `/kcd debug
castbar` is player-visible. There is nothing to look at. A `_G.X` read and a
bare `X` read are the same table lookup in an addon file, so the twelve prefix
changes cannot produce a different frame; and the `emit` rename is entirely
local names, with all eighteen DebugDump cases in tests/test_castbar_debug.lua
driving the renamed helpers through every arm — the deep colour and record
branches included, because that suite sets `mocks.UnitExists` true rather than
taking the early return. A smoke step here would be watching output that is
provably byte-identical.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nbSbqagQQGGgyab6o86Mj
commit 894754524dd0d803a360402badc3c247769911f2
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Sep 8 14:36:49 2026 +0530
M4-16: the perf panel's close button goes back to the library
The descriptor in core/PerfSetup.lua carried a `decorate` hook, and by the end that hook
was a line-for-line copy of what libs/LibKa0s/PerfPanel.lua already does in its else arm:
the same LibKa0s-Core factory, the same TOPRIGHT anchor, the same -(TITLE_H - 18) / 2
offset. The field is gone and the library draws the control.
What a reader would otherwise get wrong is why a duplicate this exact was worth deleting
rather than leaving alone. The two arms are EXCLUSIVE — PerfPanel branches on `decorate`
and runs one or the other — so this was never two paths agreeing today and kept in step by
whoever edits both. For as long as the hook sat in the descriptor the library's own arm
never ran once, in any client, ever, and nothing anywhere would have said so once the two
drifted. The hook had earned its place: it began as
`NS.DebugLog.MakeCloseButton(frame, api.Hide)`, a two-argument call onto a three-argument
function, which is how this panel drew a multiplication sign with every suite green — a
texture path that is never built draws nothing and raises nothing. Routing it through the
addon's one wrapper fixed the drawing and made it IDENTICAL to the arm it was shadowing
rather than different from it, and that is the point at which a private copy stops being a
fix and becomes a liability.
The deletion needed one source change to be safe rather than lucky. This descriptor passed
the folder name only as `name`, and PerfPanel reads `d.addonName or d.name` — so the right
answer was reached through a fallback. `title` here is already a third string ("Ka0s
KickCD"), which is what a rename reaches for first, and the failure mode is the silent one
this addon has been bitten by once already. So `addonName = addonName` is now stated
explicitly, in the same shape and for the same stated reason as core/DebugLogSetup.lua's
descriptor. Two descriptors in one addon disagreeing about how they answer the same
question is its own trap. This is the one place KickCD's M4-16 differs from
AbsorbTracker's, where the field was already there; it is the same change MultiMeters made
for the same reason, and all three repos now end in the same shape.
tests/test_perfsetup.lua gains a case rather than having one rewritten — unlike
AbsorbTracker, nothing in this repo asserted the hook existed, so there was nothing to
rewrite and the deletion would have been invisible to the suite. It pins both directions,
`addonName` present and `decorate` absent, and then shows the REAL panel against a spy on
LibKa0s-Core's factory, because a descriptor's shape says nothing about what reaches the
screen. All three halves were watched red in the working tree before the fix landed: the
name half against the descriptor as it stood, the `decorate` half against the undeleted
hook, and the panel half against a PerfPanel.lua temporarily mutated to drop its third
argument. The vendored file was restored byte for byte (md5 94445eb4…) and libs/ is
untouched in this commit.
The item's acceptance line asks that `grep -n 'decorate' core/PerfSetup.lua` return
nothing. It does not, and deliberately: what is gone is the FIELD, and what is left is a
reasoned block naming it, which is what AbsorbTracker and MultiMeters both landed too. A
comment explaining why the hook must not come back is worth more than a grep that passes,
particularly when re-adding one silently removes the library's control instead of adding a
second.
`NS.MakeCloseButton` (core/CoreSetup.lua) has no live call site left in this addon as a
result, and the comment there said "today that is core/PerfSetup.lua's step panel", which
is now false. It stays: it is the live half of a two-sided seam whose degradation branch
publishes the same name, and tests/test_surface_parity.lua compares the whole namespace
across a live and a library-less load, so deleting one side means deleting both. The
absence is recorded at the definition rather than left to be rediscovered. If the
collection's dead-export sweep reaches it before this addon draws a close control of its
own, deleting the pair is the right answer.
No WoW client was available. docs/smoke-tests.md gains §30 — one close control, in the
corner, wearing the collection's mark rather than a multiplication sign, and hiding the
panel without canceling the run. It is worth a login even though nothing on screen is
supposed to change: the library's else arm has NEVER run in a client from this addon,
because for as long as the hook existed it could not. §26's perf-panel bullet said this
was "the one close button in this addon that the HOST builds" and no longer is, so it
moves in this commit rather than in a later sweep. Nothing here has been smoked and
nothing may be reported as passing.
Case count 844 to 845: one case added. docs/test-cases.md and the README badge move with
it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nbSbqagQQGGgyab6o86Mj
commit 606d649295bc261c13fb26a6925a2dd386ccbe26
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Sep 8 14:27:13 2026 +0530
M4-12: the load-bearing TOC positions say what resolves at them
`KICKCD-A-02` under `CX04`. The collection audit read `toc-file-§5:144`'s MUST
against all 81 lines of this file and concluded the rule was unworkable; it was
counting against the wrong denominator. `M1-STD-02` settled that upstream — the
MUST binds positions that are load-bearing, and the denominator is established
by reading the seam files and `core/Constants.lua`, not by counting TOC lines.
Every premise below was re-derived in today's tree rather than taken from the
triage, which was written against a tree ninety-odd commits back.
The item names two positions for this repo. Re-derived against the standard's
own denominator instruction, this file had FOUR, and the two extra are not
padding — each fails silently, in the client, with no test going red, which is
the exact failure class the rule exists for. All four are satisfied today by
accident of the order someone typed.
- `core\PerfSetup.lua` (the item's `KickCD.toc:55`). `modules/Cooldowns.lua`,
`modules/IconGrid.lua`, `modules/IconGrid_Render.lua` and
`modules/Castbar.lua` each hold `local Perf = NS.Perf` at file scope; the
first bracket then reads `Perf.on`, so a `Perf` captured as nil raises on the
first hot-path call rather than at load. The comment also records the second
constraint — below `core\KickCD.lua`, because the descriptor's `version`
falls back to `NS.VERSION` and every capture record stamped "v?" while this
file sat higher (`performance-§8`).
- `settings\OptionsSetup.lua` (the item's `KickCD.toc:73`; the file has since
grown a line, so the position, not the number, is what was matched). It
creates `NS.Settings` and publishes the `LibKa0s-Options-1.0` instance as
`NS.Settings.Helpers`. Seven files take that member as a file-scope upvalue
and `settings/General.lua:71` and `settings/Icons.lua:182` then CALL
`H.AddComposed` at file scope, so a later position raises during load.
- `core\CoreSetup.lua`, which the triage missed. It hangs the secret-safe
printer on `NS.Util` as `NS.Util.print`; `core/Util.lua:12` assigns
`NS.Util = Util`, a FRESH table, unconditionally. Reverse the two and the
printer is replaced rather than merged, and because all ~20 call sites read
it as `NS.Util and NS.Util.print`, the addon goes quiet instead of raising.
`CoreSetup`'s own `NS.Util = NS.Util or {}` is what makes this survivable
without an error, and therefore what makes it worth a TOC comment.
- `settings\Panel.lua`, likewise missed. It decorates the Helpers instance in
place with the members that did not generalise into the library —
`Helpers.AddComposed` (`Panel.lua:191`), `Helpers.AnchorValues` (`:294`),
`Helpers.LSMValues` (`:346`). `settings/Icons.lua:56` and
`settings/Castbar.lua:192` resolve `H.AnchorValues()` into a file-scope local
and three tab files call `AddComposed` at file scope, so `Panel.lua` is
pinned above every per-tab file independently of `OptionsSetup`.
What a reader would otherwise get wrong: `settings\Panel_Widgets.lua` and
`settings\Panel_Render.lua` publish onto the same instance and yet carry
nothing, which is correct, not an oversight. Nothing loaded later reaches any of
their members at file scope, and their position is already pinned by the
annotated line above them — `toc-file-§5`'s worked example is explicit that a
rule making every line restate its neighbour's comment produces noise the next
reader learns to skip. `core\EnvSetup.lua` and `core\PoolSetup.lua` keep their
existing conventional notes for the same reason: `settings/Slash.lua:69` does
read `NS.Version` at file scope, but across a group boundary that `§5`'s
section-header MUST already pins.
`core/CoreSetup.lua:60` cited TOC line numbers (`KickCD.toc:43, ahead of
44/49/66/67`) that had already drifted by six and that these annotations move
again. It now names the four files instead. That is the one non-TOC edit here
and it is a direct consequence of this change, not a second item.
No doc change is owed: `docs/ARCHITECTURE.md`'s load-order list already states
all four constraints in prose (items 10, 15 and 19) and no document in this repo
claims a COUNT of load-bearing positions, so there is nothing to move with the
manifest.
Comment-only; no file entry moved and no order changed. `#` lines are skipped by
every TOC parser that reads this file, including `tests/_kit/loader.lua:122`
(`not entry:match("^#")`), which is how all 844 cases load the addon — the suite
passing is direct evidence the new lines are inert. The three tests that read the
raw TOC anchor their patterns at column one (`^##`, `^core\`) and are unaffected.
No smoke step is owed and none is claimed: the client-side surface is that the
TOC still parses and still enumerates every addon file, which `docs/smoke-tests.md`
§1 Fresh install + first login already covers.
Verified: `luacheck .` 0 warnings / 0 errors in 90 files; `lua tests/run.lua`
844 passed, 0 failed; `--list` diff against `docs/test-cases.md` empty. The case
count does not move, so `docs/test-cases.md` and the README `[tests]` badge do
not move. Per Decision 5 this cycle does not ship: no version bump anywhere.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nbSbqagQQGGgyab6o86Mj
commit 58971a18b39b2d2b0be719d27f77f2d8fefb747b
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Sep 8 14:17:05 2026 +0530
M4-11: the test tree comes into lint scope
`.luacheckrc:4` excluded bare `tests/`, which is what `lint`'s own template shipped until
`M1-STD-01` amended it, so all 55 of this repo's own test files sat outside the gate. `luacheck .`
covered 35 files and reported 0/0; it now covers 90 and reports 0/0, and only the second figure
means what every RESULTS.md table has been quoting it to mean. `tests/_kit/` stays excluded for a
reason that does not generalise: it is a byte copy of LibKa0s' `testkit/`, linted there as source,
so linting the copy would report every finding twice and let the copy drift green while the
original went red — the one state `tests/test_vendor_sync.lua` exists to forbid.
The harness global is declared in a `files["tests/"]` stanza rather than in the top-level
`read_globals`, and the difference is not cosmetic. A name granted at the top level is granted to
`core/`, `modules/` and `settings/` as much as to a suite, and a shipped file reaching for the test
harness is exactly what this gate exists to refuse. It is spelled `_G.KICKCD_TEST` and not bare
because `tests/run.lua:217` writes it through `_G` and every suite reads it back the same way —
`globals`, therefore, not `read_globals`.
**This repo's stanza names one global where three siblings name three, and that is deliberate.**
AbsorbTracker, PrettyChat and WhatGroup each list their SavedVariables tables beside the harness
global because a suite there writes `_G.<Addon>DB = nil` to assert on the absent-saved-variable
path. No suite here does: the fixtures hand each instance its own `inst.mocks.KickCDDB`
(`tests/test_color_shape.lua:154`), and the only mention of `KickCDPerfDB` in the tree is
`tests/test_perfsetup.lua:60`, which asserts on it as a *string* inside the TOC. Copying the
siblings' list verbatim would have granted a permission nothing uses, which is how a stanza stops
describing the tree it guards.
Narrowing the exclusion turned the tree red with 81 warnings. Forty of those were one thing: every
`KICKCD_TEST.<field> = ...` in a suite read as `(W122) indirectly setting read-only field ... of
global _G`, because the harness global was undeclared. The stanza is what answers those — a
declaration, not a suppression. The remaining 41 are real findings in this addon's own code and all
41 are fixed rather than ignored. An ignore wide enough to silence the wall would read as coverage
in every table quoting the 0/0 and provide none, which is worse than the exclusion it replaces.
Nineteen were positional discards spelled with an underscore *prefix* — `_NS`, `_p`, `_relativeTo`,
`_point`, `_s` — which luacheck does not treat specially; only bare `_` is exempt. The linted half
of this repo has always used bare `_` for a discarded return (`core/Database.lua:219`,
`modules/IconGrid.lua:1001`), and reserves the `_name` prefix for file-private locals that ARE read
(`modules/IconGrid_Render.lua:36`). The test tree had been quietly using the second convention to
mean the first for as long as nothing checked it. They are now bare `_`.
Twelve were redundant re-bindings that shadow a file-level name. Ten of those — seven in
`test_schema.lua`, two in `test_options_panel.lua` — were byte-identical: `local NS = T.NS` inside a
body whose file already binds exactly that at line 3. Deleting them changes nothing at runtime and
removes the question of whether the inner one is different. The other two ARE different, and that
is why they mattered: `test_options_panel.lua:571` and `:619` bound `NS` and `H` off a
separately-loaded `inst`, so `H` inside those two bodies was a different table from the `H` every
other case in the file uses, under the same name. They are `iNS` and `iH` now.
Nine were dead bindings, each a reader's trap rather than a style point. `test_units.lua:5` and
`test_castbar_helpers.lua:14` destructured an assertion nothing below them calls; in a file whose
whole vocabulary is assertions, an unused one reads as a case someone forgot to finish.
`test_perfsetup.lua:98` took the Castbar module and never drove it, in a case that exists to prove
which brackets were reached — so the binding claimed a coverage the case did not have.
`test_options_panel.lua:790` read `libs/LibKa0s/Options.lua` into a local and dropped it; the loop
four lines above already reads that same file along with its two siblings, so the second read is
what was left behind when the single-file check was generalised.
`test_coresetup.lua:367` is the one worth arguing about. It saved `frame.AddMessage` and never
restored it, sixty lines below a case at `:309-312` that saves and restores the same field
correctly. The restore was NOT added, because the two cases are not the same case: `:308` reaches
for the file-level `mocks`, shared by every case in the file, where the restore is load-bearing;
`:366` reaches into a `T.load(...)` instance created three lines earlier and discarded at `end)`,
where it restores a table nothing will read again. Adding a no-op restore to satisfy the warning
would have made the save/restore pattern mean less everywhere it does matter. The dead save goes.
One was a stub lying about its own signature: `test_cooldowns.lua:234` replaced `PollSpell(self,
id)` with a function that answers the same table for every id. Naming a parameter it never reads
told a reader the fixture varies by spell. It takes no arguments now, which is what it does.
One was a vestigial accumulator: `test_schema.lua:277` built a `seen` set that `:287` wrote and
nothing read — `closed` is what the group-contiguity assertion actually consults.
`test_icongrid_render.lua:36` unpacked four channels from the array colour shape and asserted only
the first and last, which is what left `g` and `b` unused. The honest fix was the assertions, not
the discard: the hash-shape line directly below it already asserts all four, the array-shape case
had simply been written to spot-check the ends, and `SafeUnpackColor` returning the middle two in
the wrong order is precisely the defect a positional unpack test is for. The four literals are read
straight off the fixture, so nothing is invented.
The last is the only suppression in this change and it is one line wide.
`test_perfsetup.lua`'s `firstWatchedSpell` returns on its first iteration, and `(W512) loop is
executed at most once` is right about the control flow and wrong about the intent: that IS how you
take an arbitrary element of a set in Lua, and the function's own comment has said so since it was
written. It carries `-- luacheck: ignore 512` with that reasoning above it. The file's *second*
copy of the same idiom, open-coded at `:576`, needed no directive at all — it was duplicating the
helper defined 549 lines above it in the same file, and now calls it.
`docs/common-tasks.md:213` told a reader that `tests/` is excluded outright, which was true when
written and is now the opposite of the rule; it is corrected in the same commit and names the one
directory inside the tree still carved out and why. A stale scope note is worse than none, because
it is the document someone consults before trusting a clean run. `:216` said "there is no live
example in this repo right now" of the inline-directive form. That sentence was true this morning
and this commit falsifies it, so it now names `firstWatchedSpell` — the doc's own point, that the
inline form is for a warning that is wrong about one file, finally has something to point at.
`CLAUDE.md:90` still reads true: it claims 0 warnings and 0 errors and makes no claim about scope.
No case count moves — 844 passed, 0 failed, before and after — so `docs/test-cases.md` and the
README `[tests]` badge are untouched, and `--list` still matches the document byte for byte.
Nothing here is reachable from the client. `.luacheckrc` is not shipped, the test tree is not loaded
in game, and no file under `core/`, `modules/` or `settings/` is touched by this commit — so it adds
no `Smoke, session N` step to `docs/smoke-tests.md`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nbSbqagQQGGgyab6o86Mj
commit 94db22577eb2150370bbdd4d4bf4824fa32e7d56
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Sep 8 14:06:35 2026 +0530
M4-09: the parity cases name the surface instead of rebuilding it
KickCD is one of the three repositories that already had a
tests/test_surface_parity.lua, so this item is the rewrite half of the row rather
than the addition half. The three LIBRARY-BACKED cases -- DebugLog, Slash's cli
and Options -- move onto Kit.assertSurfaceParity's by-name form, which arrived
with kit 15 in M4-01. Core does NOT move, and the reason is worth writing down:
NS and NS.Util are this addon's own namespace, half-published by
core/CoreSetup.lua's live arm and half by its stub. There is no major to look up,
so the four-argument form is the right one. NS.Slash stays with it for the same
reason -- it is the host's own table; only NS.Slash.cli is the library's.
WHAT THE BY-NAME FORM CHANGES HERE IS MOSTLY NOT IN THE TEST FILE, and that is
the thing a reader would get wrong. AbsorbTracker's copy of this item deleted
twelve hand-written exemptions for `__`-prefixed library internals. This file
carried only two of those (__pages and __print), because KickCD had answered the
same pressure from the other side of the seam: settings/OptionsSetup.lua's stub
MIRRORED eleven of the library's internals as no-ops, with a comment saying
exactly why -- "the parity gate reads the WHOLE live surface, and a member that
exists live and not here is a hole". Kit.publicMembers drops the whole prefix, so
that reason is gone, and ten of the eleven were then no-op members with no caller
anywhere in this addon: copies waiting to go stale on the next re-vendor that
renames one. They go in the same commit that falsifies the sentence justifying
them, because leaving them would leave a lie in shipped source.
__panelFor is the eleventh and it STAYS, because this repo is the exception the
kit cannot know about: settings/Panel_Widgets.lua:138's OpenPageTab reads
`Helpers.__panelFor and Helpers.__panelFor(pageKey)` to pre-select the
destination page's tab. A member the host calls is a member the stub owes -- the
same sentence SetRenderer joined the no-op list under at CX03. The call is
GUARDED, so losing the member would cost a tab selection rather than raise, which
is the quiet kind of degradation nothing else in the suite would report; so the
case pins it BY HAND on the line after the parity call rather than trusting a
filter that is right about every other `__` member in the library.
The load-bearing half of the test change is one table in tests/run.lua.
Kit.expose auto-wires the mock's LibStub as the surface source, which is correct
for a repo whose stubs mirror LIBRARY TABLES; all three of ours mirror an
INSTANCE -- what lib:New(descriptor) returned. Watched fail in the working tree
with the registration removed: Options went red on LAYOUT, New and STRINGS,
DebugLog on MAX_BUFFER, New and STRINGS, and Slash on ten more. It sits BEFORE
Kit.expose deliberately -- expose registers a source only when none is registered
yet, precisely so a runner like this one keeps its own.
One consequence of that, stated rather than buried: the live half is now the
runner's shared instance instead of this file's own pristine `live` load. Same
construction either way (loadInstance(true)), but earlier suites have driven it,
and tests/test_debuglogsetup.lua:70 shows the debug console -- which stamps
_frameForTest and _toggleClickForTest onto the instance. Single underscore, so
Kit.publicMembers does not filter them and they are exempted by name with that
reason. This was watched, not predicted: it was the only red the conversion
produced.
Every gate watched red in the working tree and reverted: __panelFor deleted from
the stub gives "the stub owes __panelFor: settings/Panel_Widgets.lua:138 calls
it"; RenderRows dropped from the no-op list gives "RenderRows is missing (live:
function)"; Section set to `false` instead gives "Section is a function live but
boolean degraded", the arm a present-but-not-callable member falls into and the
one a plain "is the key set?" check waves through; ConsoleCheckbox renamed in
core/DebugLogSetup.lua and CliList dropped from settings/Slash.lua's stub New
each name themselves.
NO SMOKE STEP, and the reason is not that it is inconvenient. The only shipped
source that moved is the arm that runs when LibKa0s is ABSENT, and LibKa0s is
vendored inside this addon's own folder -- there is no in-game state that reaches
it. The ten deleted members have no caller in core/, modules/ or settings/;
`grep -rn '__panels|__bannerBand|__layoutTabs|__releaseChrome|__scrollTopInset|
__tabBand|__tabPlacement|__releaseSubTabs|__tabArtHeight|__resetTabArtHeight'`
outside libs/ returns the stub itself and nothing else. Inventing a smoke step
for a path a player cannot reach would be a step nobody can fail.
844 passed, 0 failed, unchanged either side, so docs/test-cases.md and the README
[tests] badge do not move and the --list diff is empty. luacheck 0/0.
docs/testing.md's parity paragraph is rewritten in the same commit: it claimed
the gate "walks the LIVE table", which is now true of Core alone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nbSbqagQQGGgyab6o86Mj
commit 6eddbd430e9d20fd04e3fdb5222e4db1052f7e8b
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Sep 8 13:07:14 2026 +0530
M4-13: the British spellings go, against the published list
localization-§5 now publishes a canonical BRITISH/ALLOWED pair (M1-STD-12) and
requires every mechanical gate to read it whole. This sweep is that list run over
KickCD's authored text: source comments, one locale-file comment, test case names
and identifiers, and every live doc.
Measured: 62 lines across 20 files. The audit said 51 hits in 12 files and the
triage sweep corrected that to 37 in 11, because the audit's count had folded in
frozen bundles. Both were greping colour, behaviour and grey. Against the full
published list the wider entries turn up as well — recognise, generalise,
standardise, centrepiece, judgement, acknowledgement, flavour, travelled,
totalled, labelled, cancelled — and those are what makes the figure larger than
either earlier count rather than smaller.
Two things worth knowing. `tests/test_settings_spells_editor.lua:573` is not
prose: `local cancelled` is an identifier, and §5's scope covers identifiers for
the reason the section gives — a British-spelled name sits one letter from the
Blizzard symbol beside it. And `docs/settings-panel.md:103`'s heading loses its
`-colour-` anchor. The only inbound links to it are in `docs/audits/2026-09-07/`,
which is a frozen bundle and stays as written; no live document links to it.
One line keeps its spelling deliberately. `docs/smoke-tests.md:774` is the
session-3 check that the v1.27.0 perf strings read US, and it quotes CANCELLED
and unlabelled in order to say a double-L there means the string did not come
from the vendored payload. Correcting the quote would delete the check. The
prose around it at :781 and :785 is swept.
One case name moved — the Spells editor's neighbour splice — so
docs/test-cases.md is regenerated here. The count does not move, so the README
[tests] badge stays at 844.
The plan's verification grep still returns three lines in this repo, all inside
docs/perf-analysis/ and docs/revendor/ — frozen dated bundles, which 03_SPEC.md
names as a non-goal. Add --exclude-dir=perf-analysis --exclude-dir=revendor and
it returns nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nbSbqagQQGGgyab6o86Mj
commit 121da01170c22e581f2006a2b06bbb4cb150492f
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Sep 8 13:04:42 2026 +0530
M4-25: the tab-strip case was two cases wearing one name
KickCD's last complexity warning was a test. lizard measured the anonymous
function at tests/test_schema.lua:595-631 at CCN 18 against a threshold of 15 --
29 NLOC, one warning in a repo of 2232 functions -- and automated-tests.md:143
gates a tag on suites.complexity.warnings == 0. So the one thing standing
between this addon and a passable release gate was a test case that had grown a
second half.
The seam was already written down. The comment at :613 opened with "And an
UNLINKED page's strip is untouched", and the case's own `red under` line named
two mutations rather than one: "dropping the disable pass, or applying it to an
unlinked page". A case with two mutation triggers is two cases. It set
cfg.link = true, asserted every tab disabled and desaturated, then set
cfg.link = false, re-rendered, and counted operable and dimmed buttons on the
result -- two setups, two renders, two invariants, one name and one pass/fail
bit. Whichever half broke, the runner printed the same line.
Split on that line, with no assertion added, removed or reworded. Each half now
reads its own cfg, sets the link flag it needs and restores what it found, so
neither depends on the other having run first. The prose split with the code:
the INERT-strip paragraph stays with the linked case, the "counted rather than
asserted per button" paragraph -- which explains why the unlinked half counts
instead of asserting, the library disabling the selected tab on every strip --
moves to the unlinked one, and each keeps the one mutation it is actually red
under.
Seen red, one at a time, in the working tree. Commenting out
settings/Panel_Render.lua:218's deadenStrip(buttons) fails the linked case and
only the linked case. Adding deadenStrip((ctx.__tabLayout or {}).buttons) after
the RenderTabbedSchema call at :140 -- which is what "applying it to an unlinked
page" means -- fails the unlinked case, along with five other per-unit page
cases that also cannot survive a dead strip. Both mutations reverted; the
working tree carries only the split.
What a reader would otherwise get wrong: this changes no shipped code and fixes
no defect. deadenStrip and its call site are untouched. The addon behaves
exactly as it did; what moved is that the suite can now say which half of the
contract broke, and lizard has nothing left to warn about.
Counts move together, as they must: 843 to 844, docs/test-cases.md regenerated
through `lua tests/run.lua --list` and the README [tests] badge in this same
commit. No version bump anywhere -- this cycle does not ship, and the badge is a
count, not a version.
Verified in KickCD: luacheck . 0 warnings / 0 errors in 35 files;
lua tests/run.lua 844 passed, 0 failed, 0 skipped;
lizard -l lua -x './libs/*' -x './tests/_kit/*' . -- 0 warnings, against 1
before this commit. No smoke step: no client-visible surface changed, and the
M4-25 row carries none.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nbSbqagQQGGgyab6o86Mj
commit cbadcd84199eb1c6fb90907d80deb17a0e373f60
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Sep 8 12:13:22 2026 +0530
M4-04: the first of the five private Border patches goes
KickCD is the first of the five addons to lose its core/LSMPatch.lua, and the
first to be left with no fallback if the promoted surface is wrong. That is the
point of doing this one repository at a time: five deletions, five commits, five
places for a bisect to land if the client says the sentinel is not enough.
What the file was. AceGUI's WidgetRegistry is process-global -- one slot named
"LSM30_Border" for every addon in the client, Ka0s or not. This addon,
AbsorbTracker, ConsumableMaster, MultiMeters and PanelMaster each shipped their
own copy of the same wrapper, five distinct md5s, each registering at whatever
version it found plus one. Load all five and the wrapper a Border dropdown
actually got belonged to whichever addon the loader reached last. No suite in
any of the five repos could see it, because each one loads a single copy,
registers once and passes -- and docs/smoke-tests.md section 18 checked the
alignment with KickCD alone, which is the same blind spot in the client.
This commit carries the call site as well as the deletion. M4-02 put
lib.__PatchLSM30Border() into AbsorbTracker's OptionsSetup and left the other
four for their own items; KickCD's live wiring did not have it, so deleting the
private copy on its own would have taken the fixup out of the addon entirely
rather than moving it. The call and the deletion belong in the same commit for
the harder reason too: a commit that removed the wrapper and left the gate for
later is a commit that is red on the behaviour it claims to preserve.
The gate. The new case in tests/test_options_panel.lua seeds a stand-in
LSM30_Border into a fresh instance's registry through T.load's `mutate` hook --
the only window before the addon's own sources run -- and then reads the slot
back: it must no longer hold what was seeded, it must sit one version above it,
and a second call must register nothing. Seen red on the call site alone, twice:
once with core/LSMPatch.lua still on disk, which is the proof that the private
copy was never visible to this suite (its PLAYER_LOGIN frame never fires
headlessly), and once after the deletion.
The comment on the call site deliberately does not spell the AceGUI entry point
out. C02's acceptance is a grep for that identifier over core/, modules/ and
settings/ returning nothing, and a prose mention is a hit an auditor has to read
and dismiss. Same information, worded around the token.
Docs follow the file rather than the feature: ARCHITECTURE.md and module-map.md
lose the entry and renumber their load-order lists, settings-panel.md and
smoke-tests.md section 18 now name the library member, and common-tasks.md says
plainly that its inline-luacheck-directive example is gone and the repo has no
live one left, rather than quietly citing a file that does not exist.
Watch for, in the client: docs/smoke-tests.md section 29 is the session-5 step
and it has NOT been run -- no client is available here. It is the only check
that can see the defect at all, and this run is also the first evidence that one
library-level registration dresses a dropdown in an addon that no longer carries
its own. Four addons still hold their private copies while it runs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nbSbqagQQGGgyab6o86Mj
commit a606707aed6fcf14365a8a845380bc17b74a63ab
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Sep 8 11:28:07 2026 +0530
M4-01: adopt LibKa0s v1.27.0, and wire the gate that came with it
Both payloads move whole from the v1.27.0 tag -- `libs/LibKa0s/` and `tests/_kit/`
-- and the CLAUDE.md provenance line moves with them, because that line is what
`tests/test_vendor_sync.lua` reads to decide which tag to check the bytes
against. A line ahead of the bytes and a copy taken from untagged master both
fail there, which is the point of it.
`diff -r` between a clean `v1.27.0` checkout and each vendored tree is empty
BYTE for byte, not merely content-with-CR-ignored.
TWO THINGS IN THE PAYLOAD REACH THIS REPO'S OWN FILES, and invariant 1 says a
gate and its fix land together rather than leaving the tree red between commits.
* The kit now SHIPS a suite. `tests/_kit/test_eol.lua` arrives with kit 15 --
the working-tree line-ending gate, over every path `git ls-files` reports --
and `Kit.assertSuiteInventory` fails the whole run until the runner declares
it, deliberately, so a gate cannot arrive with a re-vendor and then quietly
run nothing. It joins `SUITES` with its own `dir`, root-prefixed like every
other path this runner builds, which also satisfies the second inventory
assertion `tests/test_coresetup.lua` makes over the same list. That is the
+1 in the case count, and `docs/test-cases.md` and the README badge move
with it here. It passes on this tree: the only tracked file that is LF on
disk is the kit's own `.sh`, which `.gitattributes` pins that way on purpose.
* Options minor 8 publishes `O.__print`, the one instance print sink the shell
hands OptionsWidgets so it stops building a second one from the same
descriptor. This stub is load-completing rather than member-answering, and
the host calls nothing on `__print` -- it is the library talking to itself
across a file boundary -- so it is recorded as live-only DATA beside the
layout constants, which is exactly the decision the header of that case says
a re-vendor is supposed to force. The library's own comment claims a stub
need not mirror it because `Kit.assertSurfaceParity` skips the `__` prefix;
that is true of the kit's BY-NAME form and not of the four-argument form
this case uses, which walks every key of the live table.
No version bump anywhere: this cycle does not ship. The `[tests]` badge is a
count, not a version.
The client-only half is written into `docs/smoke-tests.md` as § 28, and the
re-vendor subset at the foot of that file now names it. NOT claimed here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nbSbqagQQGGgyab6o86Mj
commit ffc1ff8ab9160b5dc399b65281242f33cc4d6ec9
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Sep 8 11:02:46 2026 +0530
M4-C1: say when the vendored-copy diff is supposed to be non-empty
docs/testing.md gave four `diff -r` commands against `../LibKa0s` and marked two
of them "MUST be empty". Neither can be empty right now, and neither is wrong.
The library is tagged v1.27.0 and this addon vendors v1.26.0, so the library diff
reports 306 lines and the test-kit diff 947 — measured just now, not estimated —
and it stays that way for the whole of M4 until this repo re-vendors. A gate that
is red by design and does not say so trains a reader to ignore it, or worse,
invites them to re-vendor an untested library release purely to make it quiet.
What the section was missing is that the four commands compare against the
sibling checkout's WORKING TREE — whatever `../LibKa0s` has checked out — which
is a different question from "is the vendored payload the release this addon
claims". Those two questions only agree while the library has tagged nothing
newer than this addon has taken, and the gap between a release and the re-vendor
carrying it is normal rather than exceptional.
So the note names the authoritative check: against the TAG the root CLAUDE.md
names, extracted from the provenance line and pulled out of the sibling with `git
archive`. Run here it reports zero lines for both payloads, which is the answer
that means something. And it is not new work — tests/test_vendor_sync.lua already
asks precisely this inside the suite, greping the tag out of CLAUDE.md and
reading that blob from git, so a green suite has answered it. The run below is
0 SKIPPED, which matters more than usual here: those cases degrade to a skip when
the sibling or git is missing, and a skip is not a pass.
The two inline "MUST be empty" comments become "empty vs the CLAIMED tag" so the
code block no longer contradicts the paragraph under it, and the working-tree
diffs are re-framed as what they honestly answer — how far behind the library
this addon is, which is release planning rather than a gate.
docs/ARCHITECTURE.md carried the same MUST in one sentence of its LibKa0s bullet,
so it is corrected in the same commit and points at docs/testing.md rather than
restating the rule — one place to be wrong is enough.
Verified here: luacheck 0 warnings / 0 errors in 36 files; 841 passed, 0 failed, 0 skipped, 841 total; the test inventory is in sync with
docs/test-cases.md. Documentation only — no behavior, no test moves, no
case-count change, so docs/test-cases.md and the README badge stay put. No client
needed and no smoke step added: nothing here reaches the game.
No version bump anywhere: this cycle does not ship.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nbSbqagQQGGgyab6o86Mj
commit e3274f6efd3a097359989da72b74ee863c910050
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Sep 8 09:53:53 2026 +0530
M3-01: re-vendor LibKa0s v1.26.0, and keep the media rows deferred
The vendored payload moves from v1.25.0 to v1.26.0 — libs/LibKa0s/ and
tests/_kit/ copied whole out of the tag, and CLAUDE.md's provenance line rolled
in this same commit, because tests/_kit/vendor_sync.lua reads that line as an
input and a line that disagrees with the bytes is the drift it exists to catch.
Only two library files actually move: OptionsCompose.lua (COMPOSE_MINOR 2 → 3,
the Critical) and OptionsWidgets.lua (minor 13 → 14, the pooled tab art). The
kit is byte-identical between the two tags, so its copy is a no-op that was
still made, because the payloads are vendored whole or not at all.
The plan expected no KickCD code change here. That expectation was wrong, and
the reason is worth writing down, because it is the sort of thing the next
re-vendor will step on again.
LIBKA0S-A-01 — the collection's only Critical — is that the composers wrapped
O.LSMValues in a second closure while O.LSMValues already returns one, so
enumList unwrapped once, saw a function, and handed the dropdown an empty list.
v1.26.0 drops the outer wrapper: a composer now reads O.LSMValues ONCE, at
row-declaration time, and assigns what comes back straight into values.
KickCD never had the empty dropdowns, and that is not luck. settings/Panel.lua
SHADOWS the library's O.LSMValues with a host implementation that returns the
hash itself rather than a reader — a deliberate, documented deviation with its
own register entry in docs/ARCHITECTURE.md. Against the old double wrap, that
shadow cancelled the defect exactly: the composer's own closure deferred the
read, so the dropdowns filled. Against v1.26.0 it becomes the defect the tag was
cut to prevent, and the worse-shaped one — the composer reads the shadow while
settings/Icons.lua is still being parsed, so every composed media row freezes at
whatever LibSharedMedia happened to hold before any media addon has run. No
error, no empty control, nothing to look at: a list that simply never grows.
This is precisely what M3-02 says is non-optional for MultiMeters. The plan
catalogued MultiMeters' table-returner and missed KickCD's, so the same edit is
made here, in the same commit as the bytes that require it: Helpers.LSMValues
returns a closure over the body it used to run inline. The listing, the ordering
and the "Default" fallback are unchanged; only the shape moved.
Three cases were red on the re-vendor alone and are the evidence for all of the
above — test_color_shape's "an LSM-backed row resolves its values at call time"
and "every static dropdown declares its order", and test_schema's default-in-
values check, which between them found sixteen media rows that had turned into
static hashes. No case was added or renamed, so docs/test-cases.md and the
README badge do not move.
docs/ARCHITECTURE.md's register entry and docs/settings-panel.md both stated the
shadow returns a table. They now state the contract that replaced it, next to
the case that holds it there.
Smoke section 27 is new and is unrun: no client is available. It is the only
place the deferral itself can be checked, because the failure needs a
LibSharedMedia that fills after the schema files have been read.
Verified: diff -r --strip-trailing-cr and plain diff -r are BOTH empty, for
libs/LibKa0s/ and tests/_kit/, against a v1.26.0 export. luacheck 0/0 in 36
files; lua tests/run.lua 841 passed, 0 failed; --list matches docs/test-cases.md.
No version bumped anywhere — this cycle does not ship.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nbSbqagQQGGgyab6o86Mj
commit 01b535d6e1908d25965b6b611b5be58fa324d75b
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Sep 8 01:38:07 2026 +0530
M2-22: the secret arm of the castbar dump reports something, always
reportSecretNint is the DEFAULT arm of NINT_REPORT: a boolean gets one line, a
nil gets another, and everything else -- which on a 12.0 client means every
protected interrupt in combat, the case the dump exists for -- lands here. Its
entire body sat inside `if _G.C_CurveUtil and ...` with no else, so on a client
without the evaluator the dump announced the field was secret and then said
nothing more about it. Whoever is handed that paste cannot tell a dump that
could not render the value from one that had nothing to render.
Availability is a clause of the sentence, not permission to speak: the line goes
out either way and names either the evaluator or its absence. The value itself
is still only described -- never tostring'd, never formatted -- which is what
the arm was always for.
Two comment lines went with it. "Pass to FontString:SetText via a hidden frame
to render and read back. Cleanest: just say secret and trust the curve." is one
half of an argument with an approach nobody took, sitting above code that does
neither thing, and a reader has to work out it is describing a road not taken.
The case that pinned this was already here, asserting the dump printed no state
line when the evaluator was missing -- a characterization case doing its job,
which is to make the shipped shape visible rather than to bless it. It is
inverted, not added to: the two claims cannot both hold. It failed first,
reporting " duration: nil" where the secret line belongs, then 841 passed, 0
failed. luacheck 0/0 in 36 files. The four-suite run is green; its single
complexity warning and max CCN 18 are what this repo already reported before
this commit, and neither moved.
Smoke 15 gained the with-and-without-C_CurveUtil dump, UNRUN -- no client is
available here, and half of it wants a client that lacks the evaluator at all,
which Retail is not. The case count does not move: one case was renamed, not
added, so docs/test-cases.md changes by a line and the badge stays at 841. No
version moves; nothing in this cycle ships.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nbSbqagQQGGgyab6o86Mj
commit db1d396ad3e2b99d8736df42b4da4221c93c2510
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Sep 8 01:28:31 2026 +0530
M2-19: the biggest download in the collection sheds 7.5M it never needed
KICKCD-A-01 was filed as a dotfile gap -- .claude, .superpowers and .pkgmeta
missing from the ignore block -- and on that framing it is a Low. Re-measured
against git rather than against the working directory, the dotfile half is worth
exactly zero bytes: .gitignore names .superpowers/ at :29 and .claude's only file
at :28, `git ls-files` returns nothing for either, and the packager clones git.
Those lines are bookkeeping and their comments now say so, verbatim, so the next
reader does not mistake them for savings.
The real number was sitting one directory over and no audit asked for it.
media/screenshots is seven tracked files, 7,813,732 bytes, and 4.7M of that is a
single compressed .gif. It is the largest such payload in the collection, it is
project-page art the CurseForge CDN already serves, and every player has been
downloading it. CLAUDE.md and DEPENDENCIES.md go with it as developer
documentation; README.md and LICENSE stay, because players read those.
The bookkeeping lines are still required, and the reason matters. packaging.md:28
carries a strong form: every root dotfile and dot-directory present in the repo
MUST either appear in this list or be justified beside it, tracked or not. The
point is not the package size -- it is that an enumeration goes stale silently
the moment a new tool writes a new directory, and by then nobody is looking.
.pkgmeta names itself under the same rule; the standard's own minimum template
left it out until v2.39.0 and cites this file among the seven that failed the
check on a line the template never supplied.
Verified with `git ls-files` filtered through this file's own ignore: list, which
is the only check that exists here -- no repo carries .github/workflows and
packaging.md:32 puts CI out of scope, so there was never a zip to list. Run
before the edit it named nine shipped files at 7,834,268 bytes; it now reports
237 tracked files shipping, and no screenshot or dev document among them.
841 tests pass, luacheck 0/0. No version moves; nothing in this cycle ships.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nbSbqagQQGGgyab6o86Mj
commit 3b67e1d4b8bfbefe48c1c86ae5a091da0628ee67
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Sep 8 00:24:14 2026 +0530
M2-17: the Profiles and Spells pages onto SetRenderer
The Blizzard AddOns sidebar reaches a canvas panel without going through
OpenOptionsPanel, so it never meets the combat gate `/kcd config` has. That
is why the refusal lives in the library's SetRenderer instead, at the OnShow
it installs. Four of this addon's six pages have been on it since the tabbed
rewrite; Profiles and Spells parked their own OnShow and therefore had no
guard on the one path a player is most likely to take mid-fight, while
docs/settings-panel.md described a refusal covering all of them. Both now
hand their body to H.SetRenderer, and Spells stops calling
EnsureDefaultsButton itself because the library already does it first.
The thing a reader will get wrong is the timing. SetRenderer runs a renderer
on first show and then only when the page has been marked dirty, and both of
these pages need every show: Spells releases its entire widget tree back to
AceGUI's pool on hide and would draw an empty page the second time, and
Profiles re-Opens the AceConfigDialog precisely so it describes the profile
that is active now. So both say so, from their own OnHide, through
H.RefreshPanel(ctx, true) — the published way to mark a hidden page dirty,
published because a host that guessed at the private flag instead is a bug
this collection has already shipped once. Declaring a renderer does newly
put Profiles on the RefreshAllPanels fan-out, but nothing fires that on a
profile switch, so the OnHide is what keeps the behaviour rather than a
belt-and-braces second path.
SetRenderer also moves out of tests/test_surface_parity.lua's exemption list
and into the degradation stub. It had been exempted on the argument that a
page which cannot be built has nothing to render into — true, and equally
true of CreatePanel and EnsureScroll sitting stubbed beside it. That
exemption is how AbsorbTracker's stub came to omit the member outright with
its own parity case green, so it is not granted any more. Removing the
exemption without the stub member fails the case with "SetRenderer is
missing (live: function)", which is the shape this was supposed to have.
The sidebar walk is written into docs/smoke-tests.md § 14 rather than left
in the review bundle. It is a different path from the `/kcd config` check
directly above it and a green there proved nothing about these two pages,
which is worth saying at the step rather than in a directory nobody opens.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nbSbqagQQGGgyab6o86Mj
commit 2373b215a54871676419533e2d1d737e7b671d85
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Sep 7 19:21:35 2026 +0530
docs: commit the frozen 2026-09-07 review and standards-audit bundles
Produced by /wow-addon:review and /wow-addon:standards-audit as part of a
collection-wide pass over all nine addons and LibKa0s. Both passes re-ran this
repo's out-of-game suites from scratch first, so every finding rests on the
numbers measured that day rather than on the committed record.
Frozen dated artifacts. Never rewritten after the fact — a later correction
belongs in the next dated bundle, not in this one.
The consolidation these fed is Ka0sAddonsCommonTasks
docs/2026-09-07-REVIEW_AND_STANDARDS_AUDIT_REMEDIATION/, which triaged both
bundles against the code, re-graded severity by reachable impact, and dropped
what did not survive re-reading. Read the consolidation for what is actually
being done; read these for the evidence behind it.
No code, TOC, doc or config file outside docs/ was touched by either pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016nbSbqagQQGGgyab6o86Mj
commit 2e21463c352304a2e8f1f22e6bb55bd295745726
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Thu Sep 3 18:24:17 2026 +0530
docs(deps): point the `file -b` note at the line it actually lives on
The CRLF-troubleshooting entry cited docs/testing.md:107; the `file -b`
recipe sits on 108. A one-line miss, but this file's whole contract is
that every entry names a file:line a reader can open — a citation that
lands on the wrong line is the thing that costs the list its credit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P8JfG34HppHBvEHHVjCMTK
commit 2e21463c352304a2e8f1f22e6bb55bd295745726
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Thu Sep 3 18:24:17 2026 +0530
docs(deps): point the `file -b` note at the line it actually lives on
The CRLF-troubleshooting entry cited docs/testing.md:107; the `file -b`
recipe sits on 108. A one-line miss, but this file's whole contract is
that every entry names a file:line a reader can open — a citation that
lands on the wrong line is the thing that costs the list its credit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P8JfG34HppHBvEHHVjCMTK
commit 849f1710cbdfaa959b45c0df4f759f3354cc4c06
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Thu Sep 3 18:03:20 2026 +0530
Carry LibKa0s v1.25.0
One file moved: OptionsCompose.lua, COMPOSE_MINOR 1 -> 2. MasterControls takes a
`leadButton` -- one page-wide act of the host's own, drawn beside options-ui-§15's
reset buttons: into the pair's empty right half on a frameless addon, or on its
own row above the full pair on a framed one. It exists because §15 fixes the
resets' wording and the composer is the only thing that writes it, so an addon
wanting a button beside them had to keep a second copy of "Reset all settings" in
its own source.
NOT ADOPTED HERE, and the reason is structural rather than a preference: this
addon passes the composer's afterGroup tail through unchanged, with no single
page-wide act of its own for the seam to place. A leadButton with nothing to put
in it is a field set to nil. Nothing is filed as a decline -- a decline records a
decision about work that was offered, and nothing was.
Both payloads copied WHOLE from the tag (git archive v1.25.0), never file by file
and never from the sibling working tree: tests/test_vendor_sync.lua resolves the
tag the provenance line names and compares both payloads against it, so a copy
taken from a dirty checkout passes a local diff and then fails the gate. The
provenance line moves in this same commit, which is what that gate reads.
The kit is unchanged at revision 14 and is copied whole anyway: the two payloads
move together so a consumer can never hold a kit that cannot compare the library
it ships.
No cross-major skew, and no file removed upstream -- so no deletion inside libs/
was warranted.
Frozen bundle at docs/revendor/2026-09-03/.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P8JfG34HppHBvEHHVjCMTK
commit 0641f80f8d71e3639c71bbcd369cb37b30cdd35b
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Thu Sep 3 13:23:08 2026 +0530
feat(settings): one Unit picker, a link out of the linked-Focus note
Four things about the Focus link, which is one relationship the panel was
describing in four awkward ways.
THE UNIT PICKER IS ONE SELECTION. It lived on each page's ctx, so choosing
Focus on Icons and walking to Cast bar arrived back on Target: the reader
re-picked the unit on every page and nothing on screen said why it had
moved. Helpers.ViewedUnit / SetViewedUnit read one field on NS.State and
RenderUnitPanel reads it rather than defaulting from the ctx. Session-only
and deliberately NOT a SavedVariable -- where someone is looking is not
something they configured, and persisting it opens a fresh login on
whichever unit was picked weeks ago with nothing explaining it. Selecting
now publishes a STRUCTURAL refresh, because the other two pages are showing
the wrong unit the moment it changes.
THE LINKED-FOCUS NOTE IS A LINK. It named a control and the page holding it
and left the reader to find both by hand, two categories away in Blizzard's
list. The phrase naming the destination now wears Blizzard's link blue and
the line opens General already ON its Units tab -- activeTab set before the
switch, so it does not jump a frame later. It refuses under combat for the
reason /kcd config does: the category switch is protected, and calling it
under lockdown taints the panel for the session (options-ui-§2). The page's
category id comes from a new NS.Settings.categoryFor, filled by wrapping
each builder, because the library's registry drops the builder's return and
the id is otherwise unrecoverable.
Two supporting members, both in the host's own widget file: Helpers.LinkRow
(a full-width InteractiveLabel -- the whole line takes the click, since
AceGUI has no widget mixing clickable and static runs in one string, and
the colour on the middle phrase is what says where it goes) and
Helpers.OpenPageTab.
NO HOVER HIGHLIGHT ON IT. The first cut passed SetHighlight(1, 1, 1, 0.12);
AceGUI forwards that to Texture:SetTexture, whose four-number form is the
deprecated colour API, and the client painted a solid bright-green block
across the whole line on mouseover. There is no correct number to pass --
a highlight would have to be a texture path -- and the link colour already
marks the line as clickable.
A LINKED FOCUS'S STRIP IS INERT: every tab disabled, every one of their
textures desaturated. Safe precisely because no schema row sets
alwaysPerUnit any more, so every tab of a linked page draws the same note --
a clickable strip there redraws the identical page, which is the same
"looks like a control, isn't one" defect the ↑/↓ arrows had. The strip still
DRAWS, because the page must not change shape when the picker flips
(options-ui-§13). Done on the buttons TabStrip returns rather than a flag on
the tab spec; if a second page ever needs it, that is when it moves into
LibKa0s.
THE LINK'S TWO CONTROLS SHARE A LINE, [tick][Copy styling from Target],
through H.RenderGrid. They were two rows -- a half-width tick, then an
InlineButtonPair holding one button -- and a button alone on a line reads as
belonging to whatever follows it. They are two halves of one decision.
InlineButtonPair could not do it: it owns a full-width row of its own.
The harness could see none of this. Frame SetEnabled / SetDesaturated /
GetRegions and the AceGUI widget's SetHighlight were PascalCase no-ops, so
"did the page disable this?" and "no highlight" vs "a highlight nobody
meant" had no answer -- which is exactly how the green block shipped.
Verified: 841 passed / 0 failed (5 new cases, each confirmed red against the
old behavior first), luacheck 0/0 over 36 files, tests/perf.lua clean,
lizard 0 warnings at CCN 15 (OpenPageTab landed at 16 and was split).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P8JfG34HppHBvEHHVjCMTK
commit 213c77743f4f2e9962232c7169958189b7ba58e6
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Wed Sep 2 21:55:04 2026 +0530
docs(comments): correct citations the revamp left pointing at nothing
Comment text only. A comment naming a file, a line or a caller is documentation and
drifts exactly like a README, except that nothing reads it but the next person to
touch the function and no gate can see it — luacheck does not read prose and no test
covers a comment.
The recurring one is afterRestoreAll, a descriptor hook that stopped existing when
the global reset became a profile reset, still being cited by name. Alongside it: a
line number past end of file, a caller that does not call, and counts that no longer
match what the code returns.
Deliberate history was left alone — a comment explaining what a deleted file used to
do is doing its job, and rewriting it would delete the explanation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0188vqPV1edgfp1WQsZnsJaF
commit e2f386a1b5a8ec6f024b68f12af4c239531afb18
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Wed Sep 2 18:25:03 2026 +0530
docs: sync the doc set to the revamped settings panel
The settings surface moved a long way in one pass — a Master controls tab, a strip
on every page, class-colour companions, the library's composers in place of
hand-written groups, and in some addons whole pages folded into General — so the
docs describing it had drifted correspondingly far.
Counts re-derived from the schema rather than carried across, the LibKa0s payload
description corrected to the file it gained, and the documentation map reconciled
against the directory in both directions.
Documentation only: no .lua, .xml, .toc, .luacheckrc or .pkgmeta was touched, and
the suites are unchanged and green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0188vqPV1edgfp1WQsZnsJaF
commit c5682dda9cf7044cf0ca2147a796030d8b571b34
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Wed Sep 2 17:56:13 2026 +0530
feat(settings): annotations re-laid out, cast bar split, spells by drag
Annotations gains a font shadow and the requested row order. Cast bar > General
splits into Size and position and a new Icon tab, and Font gains a colour. The
Spells page gains a strip and its list becomes the shared reorder widget.
Label text rendered as an empty dropdown because it was declared type=string with
no values and no dialogControl, so the library's dispatch made a dropdown with
nothing to list. It was always meant to be free text: it now declares EditBox
explicitly, and the re-vendored library warns on the bad shape so the next one
announces itself.
Re-vendors LibKa0s v1.24.0 and adopts the settings-revamp-v2 contract: a
General page opening on a composed Master controls tab, a tab strip on every page,
a class-colour companion beside every colour control, and the canonical font,
border and bar groups built by the library's composers rather than spelled out
per addon.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0188vqPV1edgfp1WQsZnsJaF
commit 2cf78fe7f089966fa46342f857380234da069a11
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Sep 1 19:33:18 2026 +0530
Tab the settings pages, and pin the Unit picker above the strip
Every schema page was one long scroll. Icons ran 32 rows nose to tail, the cast
bar 42, and finding "auto-size" meant scrolling past everything that was not
auto-size. The library could not draw anything else until v1.23.0; it can now
(options-ui-§13 / §14), so the four schema pages adopt it.
THE STRIPS. Partitioned from the rows' own `group` field, in declaration order,
with no second list naming the tabs — the schema array's order IS the strip's:
General 2 tabs / 7 rows Master controls | Units
Icons 6 tabs / 32 rows Sizing | Layout | Visual states | Border |
Annotations | Ready glow
Cast bar 7 tabs / 42 rows General | Position | Font | Spell name |
Cast time | Interruptible | Non-interruptible
Text Label 3 tabs / 14 rows General | Placement | Font
Counts are per unit on the three unit-scoped pages. Spells and Profiles draw no
strip: one is a bespoke editor with no schema rows, the other is AceDBOptions
drawing a page this addon does not own.
THE UNIT PICKER STAYS OUT OF THE STRIP. It is the page banner (Helpers.PageBanner,
§14), pinned in the chrome band ABOVE the tabs. A unit is not a subject the page
covers, it is the SCOPE every tab on the page is read under — and the mechanics
agree with the design: a tab click clears the scroll, so a picker rendered into
the scroll looks right on the render that drew it and vanishes on the reader's
first tab click, which no static reading of the builder shows. tests/test_schema.lua
pins it into the chrome for that reason.
The hand-built unit header is gone with it — the Dropdown, the focus link tick and
the copy button that Panel_Render drew three times over, once per unit page.
There is exactly one link tick and one copy button now and they live on
General → Units, because there is exactly one question (does Focus keep its own
appearance) and the scroll is the only place left to draw them on a tabbed page.
Helpers.RerenderUnitPanel went too: it re-ran the untabbed RenderSchema and had
no callers, a second and wrong answer to how a page redraws.
DECISIONS OVERTURNED, all of them recorded ones:
- General's "Appearance" section is retired. Master scale and master alpha are
the addon-as-a-whole exactly as enable, visibility and lock are; two rows whose
labels both say Master were never a second subject, only the same subject
broken over a click.
- The cast bar's "Visibility", "Orientation" and "Sizing and Layout" sections
fold into one General tab. Visibility held a single row, and one control is a
drawer rather than a subject; the other two were halves of one question that
the schema itself already joined, since autoSize decides whether width and
height are read at all and orientation decides which of the two it overrides.
- The cast bar's text tabs lose the word "casts" — on a page called Cast bar
every tab is about a cast — and Text Label's first tab is General, not Label.
- The label show/text rows keep no alwaysPerUnit flag, so a linked Focus's Text
Label page renders the note and nothing else, matching Icons and Cast bar.
TWO LITERALS PROMOTED TO SETTINGS. The charges badge's inset was the bare
`-2, 2` in modules/IconGrid_Render.lua's SetPoint call. It is
icons.chargesOffsetX / chargesOffsetY now, defaulted to exactly those numbers so
a profile that touches neither row draws the badge where it always drew, clamped
to +/-32 on read because a hand-edited SavedVariable is not an error, it is a
badge parked off the icon with nothing to say why. Icon:ApplyChargesAnchor
re-applies both on every config change; the anchor used to be set once at widget
creation and never again.
settings/OptionsSetup.lua's degraded stub grows the new members. RefreshScalars
is NOT cosmetic there: SetAndRefresh calls it on every write now, so a missing
member is a raise inside `/kcd set`, which still works with no panel. The three
layout constants that arrived with the tabbed page — BANNER_H, CHROME_GAP, TAB_H
— deliberately do not appear: options-ui-§8 forbids a host copy of a library
constant, and tests/test_options_panel.lua scans this file for exactly that.
Docs follow the panel: settings-panel.md carries the page/tab/count table (also
pinned by tests/test_schema.lua, so the two cannot drift apart quietly),
module-map.md and ARCHITECTURE.md the render layer and the message senders,
common-tasks.md the new recipe step, smoke-tests.md the strip and banner checks.
README and the remaining docs stop calling the six top-level entries "tabs" now
that a tab is a thing INSIDE one — they are pages, and the word had to mean one
thing.
Gate: 791 passed, 0 failed, 0 skipped; luacheck 0 warnings / 0 errors in 36
files. Vendor content diff empty for both payloads.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CwXwe2nXFJD4797u1U2fxb
commit 8f301465038297fcae581766657a1a1e70109f85
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Sep 1 19:32:01 2026 +0530
Carry LibKa0s v1.23.0
Both payloads whole from the v1.23.0 tag — libs/LibKa0s/ into the ship folder,
tests/_kit/ for the kit — with the CLAUDE.md provenance line in the same commit,
because tests/test_vendor_sync.lua greps that line for the tag it compares both
payloads against and has no fallback.
Options goes 9 -> 13 and OptionsWidgets 8 -> 12. Two of those minors are the
reason this re-vendor happened at all, and neither existed in the v1.19.0 copy
this addon was carrying:
- options-ui-§13, the tabbed page: Helpers.RenderTabbedSchema plus TabStrip,
SetChromeHeight and the chrome-band internals behind them. A page's rows are
partitioned by their `group` field, in declaration order, into a pinned strip
the scroll sits under. The vendored v1.19.0 had RenderSchema and nothing else
— one long scroll per page was the only shape the library could draw.
- options-ui-§14, the page banner: Helpers.PageBanner, a control pinned in the
chrome band ABOVE the strip. It is what lets this addon's Unit picker scope a
whole tabbed page instead of scrolling away with the rows — a tab click clears
the scroll, and a picker inside it vanishes on the reader's first click.
RefreshScalars arrived alongside them and is not cosmetic: Helpers.SetAndRefresh
ends in it now, so a committed drag tick refreshes values without rebuilding the
page under the widget being dragged.
Kit revision 13 -> 14. mock_base's stubFrame tracks enabled state properly
(SetEnabled/Enable/Disable/IsEnabled) instead of letting the catch-all metatable
answer IsEnabled with the frame — Blizzard's tab groups mark the selected tab by
DISABLING it, so without this no suite in any consumer could assert which tab is
current.
DebugLog.lua and Pool.lua move on line endings only; the content diff against
the tag is empty for both payloads.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CwXwe2nXFJD4797u1U2fxb
commit 2cb566160c99905922ed964c76bab8f738b3540a
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Thu Aug 27 11:08:26 2026 +0530
Carry LibKa0s v1.19.0
This re-vendors both LibKa0s payloads at the v1.19.0 tag: the ship folder
into libs/LibKa0s/ and the test kit into tests/_kit/. The only file that
moved is Widgets.lua, which goes to widgets minor 8 and adds ReorderList
to the widget surface.
The change is purely additive, so this addon needs no code change to take
it. Nothing outside libs/ calls ReorderList today; the new surface simply
becomes available should a future settings panel want it, and the higher
minor means LibStub will prefer this copy over any older one another
installed addon vendors.
The vendored bytes and the CLAUDE.md provenance line move in the same
commit, which is what the library's release procedure requires: the line
is what tests/test_vendor_sync.lua reads to decide which tag both payloads
must match, so a copy without the line, or a line without the copy, is a
red suite rather than a silent drift.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FtWnLGKT5uimw6X63zTzr
commit f952fc63f2549f5e2bbb6ea93934a662925e4e9c
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Wed Aug 26 19:01:23 2026 +0530
Re-vendor LibKa0s v1.18.1: the landing logo stops pooling its texture
OptionsWidgets minor 7 -> 8, and nothing else moves. The library's
landing page drew its logo with frame:CreateTexture() on an AceGUI
SimpleGroup -- and a texture is not a widget, so when AceGUI recycled
that frame the texture rode along and drew again on whatever the page
used the frame for next. The settings page grew a second logo,
intermittently, on pool order alone.
Minor 8 keeps the texture on the frame and reuses it, and hides it when
the widget is released. No signature, descriptor field or spec key
moves, so this addon adopts it by carrying the bytes.
The kit is unchanged at revision 13. The provenance line moves in this
commit with the payload, which is what tests/test_vendor_sync.lua
resolves the tag from.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FtWnLGKT5uimw6X63zTzr
commit 55014e0a84319e11c4915d78c180aa64760409ce
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Wed Aug 26 16:59:31 2026 +0530
Re-vendor LibKa0s v1.18.0 and take the library's resetProfile field
`afterRestoreAll` wrapping db:ResetProfile() was this addon's own
spelling of a policy the standard forbids varying (options-ui-§12), and
eight sibling addons were writing the same lines. LibKa0s-Options-1.0
minor 9 makes it a descriptor field, so the host names the db and stops
restating the rule.
With `resetProfile` supplied the library narrows its own row walk to the
sessionOnly rows BEFORE consulting skipRestoreAll -- so the veto here is
belt to that braces on the live path, and the whole policy on the
degraded one, where there is no library to do the narrowing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FtWnLGKT5uimw6X63zTzr
commit 853afa6468067b0e7da338f8b22a2508df4137a7
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Wed Aug 26 16:36:52 2026 +0530
Adopt options-ui-§12: Reset All is a profile reset, with the house wording
The standard (v2.35.0) settles what the collection's two reset controls
do and how they ask. Reset All Settings and the Profiles page's Reset
Profile MUST be the same act -- `db:ResetProfile()` on the ACTIVE
profile, never a second walk of the schema -- and the confirmation is one
verbatim, addon-agnostic string.
Everything the old reset reached by hand lives IN the profile, so all of
it comes back with one call. `aceDBDefaults()` merges NS.DEFAULT_PROFILE
over the emptied profile, restoring anchors and every unit's `link`
flag; `db.profile.spells` goes with the profile and
Database:OnProfileChanged re-seeds it through BuildSpells on the way
back, alongside the legacy fold, the spec-key migration and the refresh
-- the same path a profile switch already takes. ResetAllPositions,
RestoreUnitLinks and Database:ResetAllSpells all leave this path and
keep their other callers (`/kcd resetposition`, `/kcd spells resetall`).
`skipRestoreAll` gains the second clause the standard requires -- the
Profiles page AND every profile-backed row -- leaving the library's walk
only the sessionOnly rows a profile reset cannot reach. The degradation
stub does both halves too: it is there because the LIBRARY is missing,
not the db.
AND THE HARNESS WAS PASSING A RESET THAT NEVER RAN. `ResetProfile` was
one of the no-op DB_STUBS, which was harmless while nothing called it.
The fake now implements it with the two fidelities that matter: the
profile table keeps its IDENTITY across a reset (wiped in place, so a
module holding NS.db.profile from load keeps pointing at the live
table), and the callbacks fire in BOTH of CallbackHandler's registration
forms -- core/Database.lua registers all three profile events in the
string-METHOD form, which a fake that stores the handler and calls it
cannot dispatch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FtWnLGKT5uimw6X63zTzr
commit 84bb10f6d821b038faaee22199c51e922d84a6b5
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Wed Aug 26 11:23:38 2026 +0530
feat(media): new logo art, plus the .png master alongside it
Replaces the logo with the new collection-wide art and adds the 2000x2000
.png master the repo was missing, so the runtime .tga can be regenerated
here rather than from wherever the render came from.
The .tga is Lanczos-downscaled at 300x300 — MAIN_LOGO_SIZE in
settings/Panel.lua, and the size the landing page draws it at, so it stays
pixel-exact.
.pkgmeta now excludes media/logos/*.png and *.jpg. Only the .tga is loadable
by the client; the other two are project-page and regeneration sources.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YPd19uJ5DWdV4mKquWCSYZ
commit 066f694f391e981991e44cdbc6ea1d0bc242cbf0
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Aug 25 17:45:18 2026 +0530
docs: catch the inventory and the badge up to the pool cases
The LibKa0s-Pool-1.0 adoption added three cases to test_icongrid_buildlist.lua
and neither the generated inventory nor the README badge moved with them, so
both have been reading 780 against a suite of 783 since acf4a82. Regenerated
docs/test-cases.md with `lua tests/run.lua --list` and took the badge to
783/783 in the same change, which is the lockstep the pair is supposed to keep.
Also corrects a count the adoption left behind: core/PoolSetup.lua's degraded
arm republishes FOUR keyed members, not three — CountsKeyed is in the fallback
table beside NewKeyed, AcquireKeyed and ReleaseAllKeyed. docs/ARCHITECTURE.md
said three in both places it describes the seam, and docs/module-map.md in the
one place it does; all three now say four. Note that nothing in this addon
calls CountsKeyed today, which is worth a decision but not a silent deletion.
luacheck 0/0 in 36 files, suite 783/783. Both vendor diffs read as expected:
content empty, bytes differing on line endings only.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGhst8Q5wkcqhtnXYjMAQ8
commit fc3be836ae4c2481176125cba22660455739d0d4
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Aug 25 17:30:11 2026 +0530
Re-vendor LibKa0s v1.17.0
Pool minor 3 makes ReleaseAll park the active set backward so a position gets its own
object back. It cannot reach this addon: IconGrid is the collection's only KEYED
consumer, and where the key is the mapping there is no position to preserve. Minor 3
says so explicitly now — ReleaseAllKeyed's order is documented as carrying no meaning.
No adoption change; the re-vendor keeps the payload consistent across the collection.
luacheck 0/0 in 36 files, suite 783/783.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGhst8Q5wkcqhtnXYjMAQ8
commit cef35d4beb0b8eeb4e977a3efd11d5056a369895
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Aug 25 16:05:28 2026 +0530
docs: correct the adopted-major count and the test-kit doc revision
Comment-only corrections found by the doc sync; no behavior changes. Lint and the full suite pass unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGhst8Q5wkcqhtnXYjMAQ8
commit 8bc4ee115581dbf9127571498aa9db64867676d9
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Aug 25 15:36:56 2026 +0530
docs: catch the docs up to the LibKa0s-Pool-1.0 adoption
acf4a82 added core/PoolSetup.lua and its TOC load line without any doc
following it, so the pages that count seams had gone stale.
- CLAUDE.md: seven adopted LibKa0s modules -> eight, with core/PoolSetup.lua
added to the one-setup-file-each list.
- docs/ARCHITECTURE.md: Pool added to the adopted-majors and setup-file lists;
the degradation clause now names all three seams that report no absence
(Env, Media, Pool) instead of Env alone; core/PoolSetup.lua inserted into
the load order at its real TOC position and the rest renumbered;
perf-analysis/README.md moved to the verification-and-record table; and
docs/revendor/ named in the frozen-directory list so its two files are no
longer unregistered.
- docs/module-map.md: EnvSetup, PoolSetup and MediaSetup added to the
directory tree (all three were missing); a TOC load-order entry for
PoolSetup; and LibKa0s added to the external-dependency roll-call with its
eight adopted majors.
- docs/testing.md: the surface-parity seam list gains Perf, matching the five
cases tests/test_surface_parity.lua actually carries.
Documentation only - no .lua/.xml/.toc/.luacheckrc/.pkgmeta byte touched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGhst8Q5wkcqhtnXYjMAQ8
commit acf4a82dbfe8784a7b0b5b0de2d3c8c8a3e0fc67
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Aug 25 14:30:07 2026 +0530
Adopt LibKa0s-Pool-1.0 for the icon-grid pool
The adoption waited for Pool minor 2, and the wait was not preference. `active`
here is a map keyed by spellID, and the keying is load-bearing: it is the O(1)
index the SPELL_STATE fan-out uses on every cooldown-state message, and it is
what makes the one-widget-per-spellID invariant enforceable. Minor 1's `active`
was an array.
Porting to minor 1 anyway would have been worse than not porting. Its ReleaseAll
walks `for i = 1, #active`, which over a keyed table iterates nothing, so every
icon would be hidden, none freed, and the factory called forever — the exact leak
the library exists to end, reached through its own documented API, with the grid
still drawing and the suite still green. LibKa0s#13; minor 2 added the keyed
members and made ReleaseAll raise on a keyed pool.
What stays host code is everything about an ICON rather than a pool: the five
stamped fields, the cooldown-text ticker unregistration, the cooldown clear and
the glow stop. One behavior improved — AcquireKeyed on a live spellID returns the
button already there, where the old code took a second from the free list and
orphaned the first.
Two characterization tests added, because the failure this prevents is invisible
to every other case in the file: a release/rebuild cycle must allocate nothing,
and every widget drawing afterwards must be one that already existed. Both would
pass a pool that leaked before they were written; neither passes one now.
Closes #13.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6ncqntR9JgUoSgPf8PmCS
commit 39e259588aff3c0a75c1e8e991e8b4f1c9f668f4
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Aug 25 14:26:32 2026 +0530
Re-vendor LibKa0s v1.16.0
Both payloads whole, from the tag, with the CLAUDE.md provenance line in the same
commit — tests/test_vendor_sync.lua reads that line to pick the tag it compares
against, so a split would fail the gate.
Pool minor 2 (keyed pools, and ReleaseAll now raises on one instead of silently
recycling nothing), Widgets minor 7 (two optional CopyWindow fields, both absent
by default), DebugLog minor 12 (its copy window is Widgets.CopyWindow now, with a
hard NEEDS_WIDGETS floor), kit revision 13 (CreateFrame records its arguments, so
a suite can ask what a frame was named).
Nothing in this addon's own code changes. Lint and the headless suite green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6ncqntR9JgUoSgPf8PmCS
commit 0c668b06b13c6006aee3f19149c7e92c37285d6d
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Aug 25 12:32:37 2026 +0530
Record the 2026-08-25 LibKa0s re-vendor
The re-vendor found an empty delta: this addon was already carrying LibKa0s
v1.15.0, every shipped file at the tag's minor, both payloads byte-identical to
`git archive v1.15.0`, and test kit revision 12 paired as the rule requires. No
bytes were copied and the provenance line did not move.
The bundle is the record of that. 01_DELTA.md carries all five reads with the
command that produced each, including the consumption map and the majors this
addon does not consume; 05_SUMMARY.md carries the gate results that confirm it
independently — the addon's own tests/test_vendor_sync.lua passes both its
assertions inside a full green run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6ncqntR9JgUoSgPf8PmCS
commit f813774d10b2d65c7706ea4c2f1e605562498d4d
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Aug 25 11:03:34 2026 +0530
docs: record the fast green gate — 5.66s over 780 cases, badge to 780
The testkit revision 12 that arrived with the LibKa0s v1.15.0 re-vendor
(9907d8d) carried a compiled-chunk cache in the loader, and this is the run that
records what it bought here. tests: 5.66s wall, 29% CPU, 780 passed / 0 failed.
The previous recorded run in this repo, 20260807-114618, took 45.2s over 756
cases. Nothing in tests/ changed for it; the runner is the same file.
--jobs stays off. tests/run.lua passes Kit.run no `jobs` opt, so the suite runs
serially, and at 5.66s it is under the ~10s threshold testing-§14 sets for
reaching for sharding at all. Sharding a suite this size buys process startup
and costs a readable failure ordering.
THE BADGE WAS THE ONLY DRIFT. docs/test-cases.md regenerates byte-identical
against the suite, and its Totals row already read 780; the README [Tests] badge
was still at 774, which is exactly the silent-staleness the badge rule exists
for. Both now say 780, and `diff <(lua tests/run.lua --list) docs/test-cases.md`
is empty.
Three file:line citations in DEPENDENCIES.md were repointed at the same time,
all of them moved by the revision-12 copy or by ordinary drift, none of them
resolving any more: tests/_kit/loader.lua:31 -> :72 (the setfenv call in
Loader.load, pushed down by the chunk cache), tests/test_coresetup.lua:213 ->
:214, and docs/testing.md:100 -> :107 for the `file -b` troubleshooting line.
docs/ARCHITECTURE.md's compat-layer.md trigger row said core/Compat.lua is 490
lines; it is 496.
Vendor diffs: tests/_kit is clean against ../LibKa0s/testkit. libs/LibKa0s
differs only in DebugLog.lua, by four comment lines added to the library's
working tree after v1.15.0 — this repo vendors the tag, which is what
tests/test_vendor_sync.lua compares against, so the suite is green on it.
Gate: 780 passed, 0 failed, 0 skipped; luacheck 0 warnings / 0 errors in 35 files.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6ncqntR9JgUoSgPf8PmCS
commit 343f430c58f8f34e4327a4419e67171fafc989bf
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Aug 25 04:12:29 2026 +0530
refactor(env): read the TOC through LibKa0s-Env-1.0
Three inline C_AddOns ladders, none of them in core/Compat.lua — which is why an
audit of the shim files counted six copies across the collection when there were
eleven. A test now fails if a fourth appears.
core/EnvSetup.lua publishes NS.Meta(field) and NS.Version(); core/KickCD.lua's
`version` verb, settings/Slash.lua's NS.Slash.Version and core/PerfSetup.lua's
descriptor now resolve the same function instead of three copies free to drift.
The seam stops at C_AddOns rather than reaching for the deprecated bare global
the reference seam carries, because none of the three copies did either
(architecture-§1) — a degraded install gets exactly what it got before.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012gBvKiVQXxvbMBzpFYTbDN
commit 9907d8dc0755afbd1cc72c4d28d0e5150b003696
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Tue Aug 25 03:49:31 2026 +0530
chore(libs): re-vendor LibKa0s v1.15.0, and take testkit revision 12 with it
Adds LibKa0s-Env-1.0, LibKa0s-Pool-1.0 and LibKa0s-Item-1.0 to the payload,
takes Widgets to its own file and DebugLog to the 1500-line console buffer.
Nothing in this addon adopts the new majors yet — this commit is the copy and
the provenance line.
tests/run.lua needs no edit for the three new files: this runner derives its
library load list from libs/LibKa0s/LibKa0s.xml via Loader.xmlFiles rather than
hand-listing it, so Env, Pool and Item are picked up in XML order for free.
THE TEST KIT MOVED, and it is a debt, not new scope. LibKa0s' testkit/ is
untouched by this release; the kit went to revision 12 back at v1.14.0
("testkit revision 12: the green gate stops waiting on the disk", 2c3a39c) and
this addon never took it. It has to move now because tests/_kit/vendor_sync.lua
registers two cases — one for libs/LibKa0s, one for tests/_kit — and both are
compared against the single tag bundledVersion() greps out of CLAUDE.md. One
provenance line governs both payloads, so rolling the line to v1.15.0 obliges
the kit to move with it; there is no green state otherwise. Revision 12 is
backward compatible (a compiled-chunk cache in the loader, an opt-in
--jobs/--shard defaulting to 1), so nothing in tests/ changes for it and
docs/test-cases.md regenerates byte-identical.
The one behaviour change is the console history depth, which is the library's
and arrives with the copy. The three live places that stated the old 500 —
core/DebugLogSetup.lua:5, docs/module-map.md:232 and docs/smoke-tests.md:609,617
— move to 1500 with it. The frozen docs/audits/ and docs/automated-tests/
bundles keep their 500s: they are records of what was true when they were cut.
diff -r against the source repo is empty for both payloads.
Gate: 774 passed, 0 failed, 0 skipped; luacheck 0 warnings / 0 errors.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012gBvKiVQXxvbMBzpFYTbDN
commit 0ed27c1e2a8407e1cd0a0ff7b89cf8dcf41f17d4
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Mon Aug 24 00:48:50 2026 +0530
Adopt LibKa0s-Media: shared art on the windows, one copy of the font
The debug console's title bar drew the words Copy and Clear beside a
multiplication sign, and the perf panel closed with the same sign. All three
controls are LibKa0s' to draw, and since v1.10.x the library draws this
collection's own marks instead — but only when it is told which addon folder to
build a texture path from. It is vendored: there is no one path to it, and a copy
cannot know which folder it was copied into. So this is the wiring.
TWO SEAMS NOW SAY WHO IS ASKING. core/CoreSetup.lua wraps MakeCloseButton to
supply the folder name, once, so every close control this addon builds gets the
same mark rather than each call site remembering; core/DebugLogSetup.lua passes
`addonName` in the console descriptor, beside `name` and explicitly not instead
of it. They are the same string here and different questions everywhere — `name`
seeds the frame globals, `addonName` is a texture path — and a host where the two
diverge hands the library a path into nowhere, which draws nothing and raises
nothing.
core/PerfSetup.lua was the live version of that hazard. It called
NS.DebugLog.MakeCloseButton(frame, Hide) — two arguments onto what is two
arguments in the copy we shipped yesterday and three in the one we ship today.
Correct, green, and silently wrong the moment the payload moved: the panel would
have kept its multiplication sign while the console beside it drew art, with
nothing to report it. It is the only window in this addon whose close button the
host builds, so there was no second surface to notice on. It goes through the one
wrapper now, and a case walks the TOC to prove nothing else reaches around it.
THE FONT STOPPED BEING OURS. media/fonts/ held a copy of JetBrains Mono and its
OFL license, and five sibling addons held the same two files. Two copies of a
face is two licenses to track and two provenance stories, and a collection whose
addons stop looking like one author's work the first time one copy is regenerated
and the other is not. The face ships inside the library payload now, at
libs/LibKa0s/media/fonts/, and core/MediaSetup.lua resolves it. Its TOC line sits
above core/Constants.lua and says so in a comment: Const.FONT_MONO is resolved at
file load, so the seam has to be published first. The fallback is the client's own
STANDARD_TEXT_FONT and never a constructed path — SetFont accepts a path to a file
that is not there, fails to load it, and the text simply does not draw.
The LibSharedMedia registration moved with the bytes rather than being deleted. It
used to sit above the DebugLog guard in core/DebugLogSetup.lua on the argument that
exposing the face to other addons is not the console's to skip just because the
console is missing. That argument still holds; it just is not the console's job,
it is the media seam's, and it is guarded on the library that owns the bytes.
Bundles LibKa0s v1.10.2: libs/LibKa0s/ and tests/_kit/ (revision 11) re-vendored,
provenance line moved with them. The brief named v1.10.1; the library's own tag
had moved to v1.10.2 — the perf panel's no-`decorate` path had the same dropped
argument one window over — and the vendored gate compares the line against the
payload, so the line follows the bytes.
The settings panel is deliberately untouched. Its widgets are LibKa0s-Options-1.0's
and iconifying them is a library change with a collection-wide blast radius,
recorded as an open evolution upstream. That includes the Spells page's own row
buttons, which still draw Blizzard chat-scroll arrows and the transmog-remove
atlas: the catalog carries arrow-thick-up, arrow-thick-down and ban, and swapping
them is a real improvement, but it is a settings-panel change and belongs with the
rest of that decision rather than smuggled in here.
Three test groups pin the ARGUMENT, not the appearance, because appearance is the
one thing that cannot fail loudly here: tests/test_mediasetup.lua for the seam and
the catalog cross-check, a CoreSetup case asserting the wrapper passes "KickCD" as
the third argument, and a DebugLogSetup case grepping the descriptor for
`addonName = addonName` beside `name`. What is drawn is checked by hand, so
docs/smoke-tests.md gains section 26 — what the three controls should look like,
that the console close and the perf panel close must match, and that a
multiplication sign means the folder name stopped being passed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXv7Vni5ygBbZviZxcygqs
commit 26cb3c635e33d96068ed2ffd104c3bb4420e16c9
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Wed Aug 19 20:33:50 2026 +0530
fix(icons): classify GCD vs real cooldown by total length, not remaining
Reported from the field: the icon read "ready" a second or two before the
interrupt actually came off cooldown — small, but it is exactly the window
the addon exists to show.
The alpha, tint and swipe-suppression curves all stepped at GCD_UPPER (1.6s)
against :EvaluateRemainingDuration, so the final 1.6s of a real 15s cooldown
was numerically indistinguishable from a 1.5s GCD lockout and rendered the
same: ready alpha, no tint, and — with suppressGCDSwipe on by default — no
swipe and no countdown either.
:EvaluateTotalDuration asks the question the curves were always asking. A GCD
totals ~1.5s for its whole life and a real cooldown totals 15s+ for its whole
life, so the classification is made once and never drifts as the cooldown
winds down. Same C-side path, same secret-value safety; a client without the
method falls back to the old approximation rather than erroring, since
docs/midnight-quirks.md records that surface as a measured snapshot.
docs/smoke-tests.md had written the defect down as an expectation ("the icon
brightens ... in the final ~1.6s"); corrected along with the data-flow,
common-tasks, message-bus and testing prose.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6a4WE4mm351dnCyk5RxeQ
commit 6637c0bc04fe00684c04e7714e7de72066166c4c
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Fri Aug 7 15:33:27 2026 +0530
docs(perf): record the first in-game capture — 20260807-131311
KickCD 1.2.1, solo world combat in Falconwing Square, both arms
combat-gated with no /reload between them.
The frame-time delta is -0.17 ms/frame: inside the resolution floor
AND sign-backwards, so it is unresolved, not null. The buckets are the
reading. Accounted cost is a RANGE — 6.30 ms/s if the declared nesting
holds, 12.99 ms/s if it does not — because every Perf.Note call site
passes two arguments, so containment is declared but never observed.
Closing that gap is the analysis' first action. This bundle also makes
core/PerfSetup.lua's nesting decisions citable against a committed
record instead of the early uncommitted capture they were taken off.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BXwmAApDpktnvPrFYw3Cf1
commit 2d054919937ce9972afa23af0b42df3b7b87d7f9
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Fri Aug 7 14:01:26 2026 +0530
docs(perf): replace docs/perf-runs/ with the frozen docs/perf-analysis/ bundle store
An in-game capture and the reading taken off it used to live apart: the JSON
record went into `docs/perf-runs/` as a flat `<YYYY-MM-DD>-ingame-<label>.json`,
and whatever conclusion it supported was written wherever the decision was
made. Nothing held the two together, so a record could outlive its
interpretation or drift from it silently, and `performance-§8`'s rule that an
interpretation without its record is an assertion had no structure enforcing it.
Captures now land as one frozen dated bundle per capture at
`docs/perf-analysis/<YYYYMMDD-HHMMSS>/`, carrying exactly three files:
`report.md` (what the client printed, plus the run's lifecycle lines),
`dump.json` (the record verbatim, one line, never re-encoded) and `ANALYSIS.md`
(the write-up). The stamp is local time rendered from the record's own
`timestamp`, so a capture analysed a week later still sorts where it happened.
A committed capture with no reading beside it is now an anti-pattern in its own
right.
The store is still empty here — no `/kcd perf` capture has been committed yet —
and the README says so plainly rather than reading as though records exist.
The rewritten README also states something a reader needs BEFORE reading a
KickCD report: all eleven `Perf.Note` call sites across the eight bracketed
buckets pass two arguments and no `parentKey`, so `observedWithin` is never
populated and the three nesting claims declared in `core/PerfSetup.lua:93-102`
print as "declares itself within X — not observed". They are reasoned, not
measured, and an `ANALYSIS.md` must not subtract a declared child from its
declared parent as though the overlap were confirmed. Threading `parentKey`
through those call sites is named as a legitimate follow-up.
Documentation only, plus one comment in `core/PerfSetup.lua` that named the old
path. No behaviour changes and no code changes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FgcWFzX8WzaWuDHSU9kqZp
commit babcabab3c5e8b4829d808f33e5a5bb62d75a9fa
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Fri Aug 7 12:08:27 2026 +0530
automated-tests: record run 20260807-114618 — green
Four-suite run through the vendored kit (LibKa0s v1.8.2, test-kit
revision 10). lint 0 warnings / 0 errors over 33 files; tests
756 passed, 0 failed, 0 skipped; perf 5 scenarios;
complexity 0 warnings, max CCN 15 (recorded, non-gating).
First bundle written since kit r10's normalize_eol pass landed, so it
is also the evidence that a run no longer leaves LF stragglers in a
CRLF-pinned repo: every file in the bundle has CR count == LF count.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LE5Tjrv46E2iZfFVS47WmK
commit 56e80892ce90640d0a16cbd232b7c142aef49ea2
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Fri Aug 7 11:37:49 2026 +0530
chore(gitattributes): re-sync to the canonical body updated in v2.28.1
line-endings-§5 requires .gitattributes to match the canonical body
byte-for-byte. Correcting §7's working-tree check also rewrote the
verification advice inside that body — `file <path>` cannot report line
terminators for JSON or for a binary, so it passes files it never
examined — and no repo was re-synced when that landed.
Comment text only; no rule, pin or carve-out changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LE5Tjrv46E2iZfFVS47WmK
commit 710e32936a3d59c936033be1b0867ba72788e906
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Fri Aug 7 11:08:30 2026 +0530
Re-vendor LibKa0s v1.8.2; test kit to revision 10
Both payloads move together from the annotated tag v1.8.2 (ac857fd), and
CLAUDE.md's provenance line moves in the same commit — which is what
tests/test_vendor_sync.lua reads and resolves. The previous attempt failed
exactly there: it vendored from untagged master while the line still said
v1.8.1.
What the new bytes buy is a runner that stops littering. Everything
run-automated-tests.sh writes goes down a plain shell redirect, and a redirect
bypasses git's clean/smudge filters entirely, so in this CRLF-pinned repo every
bundle landed LF on disk while .gitattributes said CRLF. git status never
mentioned it and `git add --renormalize` never fixed it, because the index was
already right and only the working tree was wrong. Revision 10 adds one pass at
the end of the run that asks `git check-attr text eol` per path and converts
accordingly, skipping any path whose text is unset so a binary-marked asset is
left alone. It runs once over the whole bundle rather than at each write site
because perf.json is written by tests/perf.lua through --out, not by the runner.
libs/LibKa0s/Options.lua carries a comment citation fix (options-ui-§41 ->
options-ui-§1); Kit.VERSION goes 9 -> 10.
Run 20260807-110522 is the proof, and it is the first bundle here written by the
fixed runner: all seven files CRLF on disk, CR count equal to LF count. Lint,
tests and complexity artifacts are byte-identical to 20260807-022824 — a
re-vendor should move nothing else, and it did not.
lint pass 0 warnings / 0 errors in 33 files
tests pass 756 passed, 0 failed, 756 total
perf pass 5 scenarios (recorded, non-gating)
complexity pass 0 warnings, 15533 NLOC / 2059 funcs, max CCN 15
test_vendor_sync's two cases pass. Working-tree strays measured 0 before and 0
after, by line-endings-§7's corrected check.
commit 76726f03a5a0a52c58ef45c8e3da5f2eb624ac03
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Fri Aug 7 03:17:06 2026 +0530
docs: restore the canonical Standards-compliance wording in CLAUDE.md
The `## Standards compliance (read first)` section had drifted from
documentation-§6 item 3 in two ways that matter to how an agent behaves here:
- Development was "measured against" the standard, where the canonical wording
grades it a MUST — a softened MUST reads as advisory.
- The accepted-deviation arm sent the reader to "a justifying comment", but the
register moved: documentation-§3 makes `docs/ARCHITECTURE.md` →
`## Documented deviations` the single home, and a deviation not in that table
is not ratified. The upstream arm now names WowAddonStandards explicitly.
Substance taken from the fetched documentation.md (standard v2.28.0, 2026-08-06);
the section's own voice, bullets and surrounding prose are unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LE5Tjrv46E2iZfFVS47WmK
commit f84dc07efcf6c4767d07ee4c5613342d96eba846
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Fri Aug 7 02:36:06 2026 +0530
docs: correct six stale comment citations; record run 20260807-022824
Two authorised fixes.
Comment-citation corrections (comment-only; no line that runs was
touched). Each named a path or member that resolves to nothing today,
and each has exactly one determinable successor:
core/Constants.lua:94 modules/DebugLog.lua -> core/DebugLogSetup.lua
defaults/Profile.lua:9 Database:InitDB -> Database:Init
modules/IconGrid_Layout.lua:9 core/IconGrid.lua -> modules/IconGrid.lua
modules/IconGrid_Layout.lua:18 core/IconGrid.lua -> modules/IconGrid.lua
modules/IconGrid_Render.lua:875 core/IconGrid.lua -> modules/IconGrid.lua
tests/wow_mock.lua:709 tests/loader.lua -> tests/_kit/loader.lua
Citations that read as deliberate history were left alone: a path in a
past-tense sentence ("this replaces modules/DebugLog.lua", "used to be
Castbar:ApplyConfig", "Const.PANEL_PADDING_X is deleted outright") is
correct as written and rewriting it would falsify the record.
Automated-test record. The four-suite run was stale against the tree;
20260807-022824 replaces it, green, with no suite skipped for the first
time in this record. tests/perf.lua now exists, so perf reads pass over
5 scenarios where every earlier row reads skip, and RESULTS.md's Perf
section is rewritten rather than refreshed - it asserted the addon ships
no scenarios, which is no longer true. Tests 737 -> 756, lint 32 -> 33
files, complexity averages flat (avg CCN 2.1, avg NLOC 6.5) against
+103 NLOC, zero CCN warnings held. Band steady at four files; only
tests/wow_mock.lua grew (+35, tracked as KCD-30).
commit f658a2583b7e32c0d6399ae30750188c3908639e
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Fri Aug 7 02:09:50 2026 +0530
docs: repoint DEPENDENCIES.md's stale file:line evidence
Four citations no longer resolved, which is the specific failure that makes
the toolchain list stop being trusted:
- the setfenv sandbox moved with the kit (tests/loader.lua:84 ->
tests/_kit/loader.lua:31); the old path does not exist at all
- tests/test_coresetup.lua's io.popen("ls ...") is at :213, not :169
- docs/testing.md's `file -b` troubleshooting note is at :100, not :67
- tests/test_vendor_sync.lua is five lines now, not four
Evidence only; no entry added, removed or reworded.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuCbofydwVeUp61YW4DiJ4
commit f95c0d63de21b1267db961dd06960e2558d8298e
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Thu Aug 6 21:21:28 2026 +0530
docs: retire docs/pending/LEDGER.md; the pending store is GitHub issues
Implements Ka0s WoW Addon Standard v2.26.0. The decision record lived in
the working tree, which is the one place a decision record cannot be
reached from: a deferral was invisible to the issue list the maintainer
actually opens, nothing notified on it, and nothing outside this repo
could reference it.
Every row anything still refers to is now a GitHub issue on this repo,
with status carried as a title prefix — [deferred] and [untriaged] open,
[done] and [will-not-do] closed. Deferrals and reasoned refusals migrated;
finished work did not, except where live documentation cites it by id,
because the pointer is the thing being preserved.
Every doc and comment reference now points at its issue rather than at a
deleted file. The terminal rows the file held remain in git history at
this commit's parent.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuCbofydwVeUp61YW4DiJ4
commit 90f9498dd912cca2b31c927e8551fd7a01142c3a
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Thu Aug 6 18:41:08 2026 +0530
Move the LibKa0s provenance line into CLAUDE.md; test kit to revision 9
Adopts LibKa0s v1.8.1, which is the test kit alone — no shipped library file
moved, so libs/LibKa0s/ is untouched and still byte-identical to the tag.
tests/_kit/ re-vendored from v1.8.1. Kit revision 9 changes exactly one file:
vendor_sync.lua now reads the "Bundles [LibKa0s](…) vX.Y.Z (MIT)." line out of
the consuming repo's CLAUDE.md instead of its README.md, via a new
provenanceFile opt. There is no fallback, deliberately — a repo that
re-vendored without moving its line fails the case naming CLAUDE.md, rather
than sitting half-migrated with two lines that can disagree.
So the line moves here in the same commit as the bytes it describes. It
answers "which LibKa0s does this build carry?", which is a maintainer's
question; CLAUDE.md is where this repo already keeps the facts a maintainer or
an agent needs about the build. README.md is the page a player reads, and a
vendored-library inventory was never something a player needed — so the whole
"Libraries and credits" section comes out, LibKa0s sentence, Ace3 inventory and
all. Nothing in it was an external credit, so no Credits section survives it.
Also unlinks the Standard badge in README.md; the badge markdown is unchanged.
docs/test-cases.md regenerated: revision 9 renames one case ("…the README says"
→ "…CLAUDE.md says"). Counts do not move — still 756/756 — so the README
[Tests] badge stays as it is.
docs/pending/LEDGER.md: LIBKA0S-03 recorded the provenance line as living in
README.md; noted as superseded rather than rewritten. DEPENDENCIES.md: the git
and diff rows pointed at line numbers inside the old hand-copied gate and at
README.md as the file carrying the tag; both now point at
tests/_kit/vendor_sync.lua and CLAUDE.md.
lua tests/run.lua: 756 passed, 0 failed, 0 skipped, 756 total
luacheck .: 0 warnings / 0 errors in 33 files
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y4FjrPatWaye1gFsfS4vpb
commit b0ce9479ff360c0155241a3a2318c15c8621df5a
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Thu Aug 6 17:23:21 2026 +0530
Pin line endings with the canonical .gitattributes (line-endings-§2)
This repo is client-bound, so the working tree is pinned CRLF on every
platform: `* text=auto eol=crlf`, the `*.sh text eol=lf` carve-out that
keeps the vendored test runner executable, and the shared binary block.
The body after the pin is byte-identical to every other repo in the
collection, so diffing a client-bound repo against a non-client one shows
one decision rather than two documents.
Working-tree stragglers written by tools that bypass git's filters were
re-materialized from the index; the headless suite passes unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A9nTGYaHsqCEf6obganps6
commit cf03d621731cbe04835c16f70d9a493886448256
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Thu Aug 6 16:00:40 2026 +0530
docs: adopt the documentation tier model (standard v2.23.0)
Renamed (git mv, history preserved): saved-variables.md -> schema.md, the
canonical name for the persisted shape across the collection.
Created: common-tasks.md — the last missing Tier 1 doc — which absorbs the
retired conventions.md as its "House rules" half and adds the recipes those
rules govern: add a setting, a slash command, a tracked spell, a locale
string, a message. The spell recipe states the trap that cost issue #8: spec
keys are Blizzard's NUMERIC ids written through Const.SPEC aliases, because
uppercased spec NAMES made a frFR Elemental Shaman derive "ELEMENTAIRE",
miss the "ELEMENTAL" key and track nothing at all.
Created: profiles.md — the Tier 2 trigger fires here, since AceDB profiles are
user-visible through the Profiles settings page. It records the three
deliberate choices in settings/Profiles.lua (no Defaults button, rendered into
our container rather than its own window, re-opened on every OnShow so the
page reflects the CURRENT profile after a switch) and the OnProfileReset
signature detail: AceDB passes nil as the third argument there, so the shared
handler substitutes the active key rather than trusting the parameter.
Retired: conventions.md.
ARCHITECTURE.md gains `## Documentation map`. Six of seven Tier 2 docs are
present here — the most in the collection — and debug.md is the one recorded
as "Not applicable", since /kcd debug's subcommands dump state through the
library console rather than adding a surface of their own.
Verified: 756 tests passing, luacheck clean, every relative doc link resolves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KQxehYAMgZtvAJDCJxXYRs
commit f3335df37ab7aab31b95c42a31c28c885839ba09
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Thu Aug 6 01:11:56 2026 +0530
docs: retire the two claims this changeset made false
Two facts moved under the remediation branch and their doc footprint did not
follow.
README `[Tests]` badge: 747/747 -> 756/756. The suite gained cases across
M3-13, M3-12 and M4-02 (the panel-layout metrics case, the live-bucket castTick
count, and the malformed-profile render comparison). `docs/test-cases.md` is the
authoritative count and already says 756; the badge is static and goes stale
silently, which is exactly why CLAUDE.md requires the two move in the same
change (documentation-§1 / testing-§5).
`docs/ARCHITECTURE.md` message-bus invariant: M4-04 gave CONFIG_CHANGED and
PROFILE_CHANGED one named emitter each, so the paragraph describing
CONFIG_CHANGED as "emitted from several modules" and carrying PROFILE_CHANGED's
two-emitter split as a recorded intentional deviation now describes code that no
longer exists — and that deviation is no longer in the `## Documented
deviations` register, so the paragraph was the last place claiming it. Replaced
with what the code does: one owning module per message,
`Helpers.FireConfigChanged` and `fireProfileChanged` as the single send sites,
and SPELL_STATE (3) / GRID_LAYOUT (2) keeping several sites inside their own
owning module, which is what architecture-§4 asks for. This now agrees with
`docs/message-bus.md`, which was already correct.
Verified after the edit: 756 passed, 0 failed, 0 skipped; luacheck 0 warnings /
0 errors over 33 files; lizard clean, no threshold exceeded; both vendored-copy
gates (`libs/LibKa0s`, `tests/_kit`) empty on the content AND the byte reading
against LibKa0s v1.8.0.
Not changed, deliberately: `docs/automated-tests/RESULTS.md` still reports the
`20260804-233245` run at 737/737 over 32 files. It is a generated record whose
checkpoint is the release, not this commit — `/wow-addon:bump-version` refreshes
it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KQxehYAMgZtvAJDCJxXYRs
commit f3335df37ab7aab31b95c42a31c28c885839ba09
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Thu Aug 6 01:11:56 2026 +0530
docs: retire the two claims this changeset made false
Two facts moved under the remediation branch and their doc footprint did not
follow.
README `[Tests]` badge: 747/747 -> 756/756. The suite gained cases across
M3-13, M3-12 and M4-02 (the panel-layout metrics case, the live-bucket castTick
count, and the malformed-profile render comparison). `docs/test-cases.md` is the
authoritative count and already says 756; the badge is static and goes stale
silently, which is exactly why CLAUDE.md requires the two move in the same
change (documentation-§1 / testing-§5).
`docs/ARCHITECTURE.md` message-bus invariant: M4-04 gave CONFIG_CHANGED and
PROFILE_CHANGED one named emitter each, so the paragraph describing
CONFIG_CHANGED as "emitted from several modules" and carrying PROFILE_CHANGED's
two-emitter split as a recorded intentional deviation now describes code that no
longer exists — and that deviation is no longer in the `## Documented
deviations` register, so the paragraph was the last place claiming it. Replaced
with what the code does: one owning module per message,
`Helpers.FireConfigChanged` and `fireProfileChanged` as the single send sites,
and SPELL_STATE (3) / GRID_LAYOUT (2) keeping several sites inside their own
owning module, which is what architecture-§4 asks for. This now agrees with
`docs/message-bus.md`, which was already correct.
Verified after the edit: 756 passed, 0 failed, 0 skipped; luacheck 0 warnings /
0 errors over 33 files; lizard clean, no threshold exceeded; both vendored-copy
gates (`libs/LibKa0s`, `tests/_kit`) empty on the content AND the byte reading
against LibKa0s v1.8.0.
Not changed, deliberately: `docs/automated-tests/RESULTS.md` still reports the
`20260804-233245` run at 737/737 over 32 files. It is a generated record whose
checkpoint is the release, not this commit — `/wow-addon:bump-version` refreshes
it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KQxehYAMgZtvAJDCJxXYRs
commit 1d58780df9b420905a75b7a67781bba752502289
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Wed Aug 5 23:27:04 2026 +0530
test(castbar): render a malformed profile and compare it against a fresh one [fixes M4-02 test]
The old case asserted table IDENTITY — rawequal(Castbar.INT_FALLBACK,
NS.CASTBAR_DEFAULT.interruptible) — between the two locals the commit had just
rewired and the defaults sub-tree. That is stricter than the rule it cites.
savedvariables-§2 asks for one DECLARATION site, and
`copy(NS.CASTBAR_DEFAULT.interruptible)` is one declaration site, renders
identically, and is the correct shape as soon as any consumer needs to write to
the sub-config — identity reddens on it. Meanwhile the requirement the case
states in its own comment ("a malformed profile that falls back must render
exactly like a fresh one") was never driven: the sibling cases read the fallback
tables directly, so stateConfig's fallback arm was never taken while a frame was
observed.
Now: the declaration-site case compares scalar fields against the shipped
defaults instead of demanding identity, and a new case drives the requirement.
It renders a cast on an enabled instance with a fresh profile, snapshots both
bars' status-bar colors, the name text color, both border alphas and both
backdrop border colors, then sets units.target.castbar.interruptible /
.uninterruptible to each malformed shape stateConfig exists for (key absent, and
a bare color string under the key) and asserts the render is slot-for-slot
identical.
PRODUCTION MUTATION used to prove red: re-forked INT_FALLBACK / UNINT_FALLBACK
in modules/Castbar.lua as a hardcoded second copy drifting on the interruptible
border (borderShow false, borderSize 1) — the exact historical bug. RED: "a
profile whose per-state config is absent must render identically to a fresh one
(slot 13) (expected 1, got 0)".
Also verified the new pair does NOT redden on correct code the old one did:
replacing the shared references with a shallow copy of the defaults keeps both
cases GREEN at 756 passed. Reverted: 756 passed, 0 failed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
commit f78e3a16f936552bc3f253033930c3b5357d2e9b
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Wed Aug 5 23:19:56 2026 +0530
test(perf): count castTick's exits off the live bucket, not off the source text [fixes M3-12 test]
The leak case asserted a line-based source scan over four modules: inside any
function that opens `__t0 = Perf.on`, every `return` must be immediately
preceded by a `Perf.Note(` line. Its target was literally the two lines the same
commit had just added, and its written justification — "behavioral coverage is
not available here… the harness drives neither" — was false.
tests/test_castbar_frame.lua:251-259 already drives exactly the leaked exit
(`inst.current = nil; inst.frame:_run("OnUpdate")`), and the case immediately
above at test_perfsetup.lua:551 already reads live counts through P.__buckets()
and asserts `b.calls == 5`. The right shape was already in this file.
Being line-based, the scan also reddens correct code: a `return` inside a nested
closure, or a bracket closed through a helper rather than a literal
`Perf.Note(`, is reported as a leak.
New case mirrors the PollSpell one. With P.on, it starts a cast on a real
Castbar instance, runs three ordinary `inst.frame:_run("OnUpdate")` frames,
clears inst.current and runs the teardown frame, then asserts
`buckets.castTick.calls` equals the total number of OnUpdate invocations — one
short is exactly what an unclosed guard exit produces. The scan is KEPT as a
supplementary lint for brackets nothing yet drives (cdText, behind a C_Timer
ticker); its comment now says that instead of the false claim.
PRODUCTION MUTATION used to prove red: deleted
`if __t0 then Perf.Note("castTick", debugprofilestop() - __t0) end` from
onUpdate's no-duration guard exit, modules/Castbar.lua:692. RED:
"every OnUpdate exit must be counted; got 3 of 4 — one short means the teardown
frame leaked (expected 4, got 3)". Reverted: 755 passed, 0 failed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
commit 5f8c7691e030f504ce230cc07ca561fc2a19ed34
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Wed Aug 5 23:15:12 2026 +0530
test(constants): assert the panel layout metrics arrive as positive numbers [fixes M3-13 test]
The commit that deleted the host copies of PADDING_X and ROW_VSPACER also
deleted the coverage that guarded them: PANEL_PADDING_X left the "every panel
metric is a positive number" loop, and test_surface_parity.lua moved ROW_VSPACER
into the live-only exclusion list. What replaced it was a source scan asserting
the host does not RESTATE a numeric literal — a correct lint, but it stays green
if the library stops publishing the value at all.
That hole is live. settings/Panel_Render.lua:20 binds
`local ROW_VSPACER = Helpers.ROW_VSPACER` AT FILE LOAD and forwards that binding
at :83 and :117; settings/Panel_Widgets.lua:49 does the same. A nil arriving
there is silent: AddSpacer creates a full-width SimpleGroup with no height, every
options row loses its spacing in game, and the whole suite stays green.
Two cases restore the guarantee, read off the instance rather than off Const.
The first asserts LibKa0s-Options publishes PADDING_X, ROW_VSPACER,
SECTION_HEADING_H and BUTTON_PAIR_REL as positive numbers on the instance
NS.Settings.Helpers IS. The second is the user-visible end of it: it renders a
unit panel through Helpers.RenderUnitPanel and asserts every row spacer the
render emits carries a positive numeric height — which is what the type check
cannot cover on its own, because the forwarding binding is taken at file load.
The source scan is kept unchanged as a supplementary lint.
PRODUCTION MUTATION used to prove red: libs/LibKa0s/Options.lua:210
`O.ROW_VSPACER = L.ROW_VSPACER` -> `O.ROW_VSPACER = nil`. Both new cases go red
("must be published as a positive number, got nil" and "row spacer 1 rendered
with height nil"); the existing source-scan lint stays GREEN under it, which is
the hole. Reverted: 754 passed, 0 failed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
commit f08ef1d7c2f71b8cb1d5a41ea26bd15a125bf484
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Wed Aug 5 15:44:23 2026 +0530
docs(deviations): re-grade the two taint-§8 findings under the scoped rule [M4-27]
M1-STD-11 was resolved as OPTION (a): events-frames-taint-§8's pre-formatting
MUST is now scoped to call sites whose arguments can reach a value read from
one of the APIs it names, and a SHOULD everywhere else. So this item is a
disposition, not a fix — no call site is converted and the cited code is
untouched. What was needed was reading the live rule against this repo and
recording the result.
Findings graded against the live §8:
- KCD-A-15 (`modules/Castbar_Debug.lua` pre-formatting) — PERMITTED, recorded as
a note rather than a register row. KickCD uses none of the named APIs
(verified: no UnitGetTotalAbsorbs / UnitHealth / UnitThreatSituation /
C_UnitAuras / UNIT_AURA anywhere in core, modules, settings), and the one
combat-protected value it does handle — `current.notInterruptible` from
UnitCastingInfo/UnitChannelInfo, which "any other API a client build protects
in combat" does put in the trigger set — is never formatted. Those lines build
from its `type()` and from `issecretvalue()`'s boolean, and the
`NINT_REPORT.boolean` arm is reachable only once the type already IS boolean.
No KickCD pre-formatting site sits on a trigger-set value, so the MUST fires
nowhere here.
- KCD-A-04 (`core/Compat.lua:373-381`'s second value renderer) — does NOT
dissolve under the scoping, which touches only the pre-formatting rule.
§8's closing line still makes a hand-rolled stringifier the deviation, so it
is recorded as a register row in docs/ARCHITECTURE.md with its rationale (a
diagnostic value column, not the printer seam; `issecretvalue()` is what the
concat probe approximates, not the forbidden `..` probe; every value reaching
the format has already been sanitized) and a re-check trigger.
Also reported, NOT fixed — out of this item's scope: `modules/Castbar_Debug.lua:125`
binds `NS.Util and NS.Util.print or _G.print`. §8's ban on the global `print()`
for user-facing output is one of the two things the scoping explicitly does not
relax. The fallback arm is unreachable today (core/CoreSetup.lua defines
Util.print on both the library-present and library-absent paths), so it grades
Info — but it is dead code reading as a sanctioned escape hatch and is written
down in the register section so it is not lost.
Register housekeeping: the stray blank line that split the deviation table into
two fragments is removed, so the "single home" the section claims is one table.
Closes KCD-A-04, KCD-A-15.
Verified: luacheck 0 warnings / 0 errors in 33 files; lua5.1 tests/run.lua
752 passed / 0 failed; lizard Warning cnt 0; tests/perf.lua 0 failures. No Lua
file changed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
commit 92511dbdd8807a5b209cc2f692d2f3e06a9672df
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Wed Aug 5 15:40:36 2026 +0530
fix(debuglog): stop the degraded stub re-spelling the library's line and hexes [M4-26]
debug-logging-§3 forbids a fallback stub from reproducing the library's line
format OR its color codes, and debug-logging-§7 still requires the stub to flip
the flag and print the ack. KickCD's stub did both wrong ways round: its
FormatPlain was byte-identical to libs/LibKa0s/DebugLog.lua's lib.FormatPlain,
and SetEnabled printed the library's own green/red state hexes — while the
file's own comment claimed "no formatter ... say nothing about color", arguing
only the color half and missing the format entirely.
- FormatPlain (and the FormatColored alias) now hand back the message verbatim.
The stub has no console, so it has no line to render, and inventing a shape
would be a second format to keep in step with the library's. Nothing in the
addon calls them; they exist for the surface parity debug-logging-§7 asks for.
- The ack still prints — "debug logging ON" / "OFF" — uncolored. The ack is the
MUST; the state hexes are the library's string, not this file's to copy.
- The :58-62 comment is rewritten to argue the right half, and deliberately does
not quote the forbidden shape, so a grep for a hand-copied format cannot find
one in the file that swore off it.
Closes KCD-A-14.
Tests: the ack case now also pins that the STATE WORD carries no color escape
(the [KCD] tag's own color is NS.Util.print's and is out of scope); a new case
pins that the stub renders no line; and the source-scanning case grew from two
probes to five — it missed both of the copies that were actually there.
Falsifiability: re-planting the old FormatPlain body and the old hex-bearing ack
reddens three cases (749 passed / 3 failed) with "the stub must hand back the
message, not a formatted line" and "must not carry the plain line's separator";
reverting returns 752/752.
Also repairs docs/test-cases.md: M4-02 regenerated it through a shared /tmp path
that a concurrently-running sibling repo overwrote, so the committed inventory
was another addon's. It is regenerated here from this repo's own `--list`, and
`--list` now diffs clean against it CR-normalized.
Verified: luacheck 0 warnings / 0 errors in 33 files; lua5.1 tests/run.lua
752 passed / 0 failed; lizard Warning cnt 0;
`grep -nE '\|cff40ff40|<ts> \| \[' core/DebugLogSetup.lua` returns nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
commit 789bb8595d4bf2faa1d2b14243e78e79f9e19b51
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Wed Aug 5 15:31:40 2026 +0530
perf(offline): ship the scenario runner and stop citing an uncommitted capture [M4-25]
performance-§9 wants an offline scenario runner at tests/perf.lua with a
zero-overhead scenario; performance-§8 wants docs/perf-runs/ with a README;
documentation-§3 wants docs/performance.md. KickCD had none of the three, so
its automated-test manifest recorded `perf: skip` — and core/PerfSetup.lua
quoted "125.02 ms / 51.14 / 73.9 ms" from a capture that was never committed,
which under performance-§8 is an assertion wearing evidence's clothes.
- tests/perf.lua drives the addon's real chain under the headless mock, with
both load lists DERIVED (LibKa0s.xml + the TOC) so a short list cannot
silently measure the degradation stub. Five scenarios: spellPoll (asserts
exactly 3 WoW API calls per watched spell), spellState, iconApply, and the
probeOverheadOff/On pair performance-§9 requires by name. Assertions are on
API counts and bytes/iteration only — never wall-clock.
- The zero-overhead pair carries three assertions: an ABSOLUTE 900-byte ceiling
on the dormant arm (measured 848.0 — the relation alone cannot catch a
regression that lifts both arms together), dormant <= armed, and equal API
counts.
- docs/performance.md: the two harnesses, the eight buckets and their declared
nesting, why `visibility` is not nested and `glowGate` not declared, the
offline scenario table, the real `/kcd perf` verb set, and the suspend
contract's two halves.
- docs/perf-runs/README.md: naming, the schema-2 sketch (every figure zeroed,
so nothing in it can be misread as a capture), the field notes, and a plain
statement that the directory is EMPTY and that is the gap.
- core/PerfSetup.lua: the uncommitted figures are removed and replaced with a
note saying why. The two decisions they backed — instrumenting both PollSpell
exits, and un-nesting `visibility` — keep their reasoning, restated so it
stands on structure (RefreshVisibility has seven call sites, six of them not
cast events) rather than on unquotable numbers.
Closes KCD-R-07, KCD-A-05, KCD-A-06.
Falsifiability: breaking the gating idiom in Icon:Apply (`local __t0 = ({
Perf.on and debugprofilestop() })[1]`) lifts the dormant arm to 928.0 bytes and
the runner exits 1 with "over the 900-byte ceiling"; reverting exits 0.
Verified: luacheck 0/0 in 33 files; tests/run.lua 751 passed / 0 failed; lizard
Warning cnt 0; `lua5.1 tests/perf.lua` 0 assertion failures; and
`tests/_kit/run-automated-tests.sh --suite perf --no-bundle` now reports
`perf pass — 5 scenarios`, not skip.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
commit ec20e81e940d8ebb4fcf9629e927c9f9d5d9dea5
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Wed Aug 5 15:20:47 2026 +0530
refactor(defaults): give the profile tree one declaration site [M4-02]
savedvariables-§2 wants every profile default declared in defaults/Profile.lua
and nowhere else. KickCD had it in core/Database.lua, and a SECOND hardcoded
copy of the two per-state cast-bar tables in modules/Castbar.lua — which had
drifted from the first in both directions that matter: it stored its colors
positionally where the defaults tree stores them keyed (the v3→v4 color-shape
migration's whole subject), and it disagreed about the interruptible border
(borderShow=false/size 1 versus true/2). A profile that fell back therefore
rendered differently from a fresh one. The duplicate is the finding.
- New defaults/Profile.lua carries ICONS_DEFAULT, CASTBAR_DEFAULT,
LABELSTYLE_DEFAULT and DEFAULT_PROFILE verbatim, published as NS.C /
NS.DEFAULT_PROFILE plus NS.CASTBAR_DEFAULT / NS.LABELSTYLE_DEFAULT for the
two callers that need a sub-tree by name. No content edit to the tree.
- KickCD.toc gains defaults\Profile.lua ahead of defaults\Spells.lua. `#
Defaults` loads after `# Core`, so core/Database.lua reaches the tree at CALL
time: the AceDB defaults table is now built by aceDBDefaults() from InitDB,
and BackfillLabelStyle reads NS.LABELSTYLE_DEFAULT inside the function.
- modules/Castbar.lua's INT_FALLBACK / UNINT_FALLBACK are deleted and read off
NS.CASTBAR_DEFAULT at load (modules load after defaults). Both consumers —
Castbar:ApplyState and Castbar_Skin:Reskin — are read-only, so sharing the
tables cannot write back into the defaults.
- Docs repointed at the new home: ARCHITECTURE.md (owner table + load order),
module-map.md (tree, load order, lifecycle), conventions.md,
saved-variables.md, smoke-tests.md.
Closes KCD-A-10.
Test: the old case asserted the DIVERGED copy's values ("gold with no border"),
so it is replaced by two: one pinning that each fallback IS defaults/Profile's
table (rawequal — equality would still pass on a re-forked copy), one keeping
the gold-vs-red distinguishability, read through UnpackColor so it does not
re-encode a shape. Falsifiability: re-planting the old positional literal as
INT_FALLBACK reddens "each state fallback IS the shipped default" (750 passed,
1 failed); reverting returns 751/751.
Verified: luacheck 0 warnings / 0 errors in 33 files; lua5.1 tests/run.lua
751 passed / 0 failed; lizard Warning cnt 0; docs/test-cases.md regenerated and
`--list` diffs clean CR-normalized.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
commit 2621a43818e6bd1bf1a5efa36518265227276ad0
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Wed Aug 5 15:06:49 2026 +0530
fix(slash): pcall the GateHint probe so the gate is restored unconditionally [M4-03]
settings/Slash.lua's NS.Slash.GateHint answers "why did that dropdown value get
rejected?" by swapping the gating setting's stored value to each candidate,
re-asking the row's own `values()`, and restoring. The comment above it records
the swap as deliberate and transient — but the one call inside the window is
addon-authored and free to raise (an LSM-backed row reads another addon's
table). If it did, the restore never ran and the swapped-in candidate stayed in
SavedVariables: a silent write produced by a READ-ONLY hint, outside the
addon's single write seam, with no onChange and no panel refresh.
The probe now runs under pcall, so the restore line is unconditional and a
raising `values()` costs the user one missing "flip X to Y" clause instead of a
changed setting. The docstring says so and points at the case.
New case in tests/test_color_shape.lua: build a row carrying a real valueGate but
a `values` that raises, call GateHint, and assert both that it does not
propagate and that H.Get(row.valueGate) is byte-identical to its pre-call value.
Closes KCD-R-06, KCD-A-17 (cluster C19+C24+C28 — one write seam; a carve-out
that writes the live profile and restores it wraps the window in a pcall).
Verification: luacheck . 0/0 in 32 files; lua5.1 tests/run.lua 750 passed, 0
failed (749 -> 750, the one added case); lizard 0 warnings; docs/test-cases.md
regenerated and `diff` against `tests/run.lua --list` is empty with both sides
CR-normalized; git status shows only the three intended paths.
Mutation: reverting the probe to `local alt = allowedKeys(row)` with no pcall
turned the new case red — "GateHint must not propagate the row's error; got:
values() raised mid-probe", 749 passed / 1 failed. Restoring the pcall returned
it to 750 passed / 0 failed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
commit 9a172d4ed86bfcc1e11ee512a4d4382ad81159c0
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Wed Aug 5 15:01:30 2026 +0530
refactor(bus): give CONFIG_CHANGED and PROFILE_CHANGED one sender each [M4-04]
architecture-§4 wants one emitter per message. KickCD had six SendMessage sites
for Ka0s_KickCD_CONFIG_CHANGED (core/KickCD.lua's lock toggle and its slash
spells commit, modules/IconGrid.lua and modules/Castbar.lua OnDragStop,
settings/Spells.lua's throttled commit, settings/Panel.lua's Helpers.Set) and two
for Ka0s_KickCD_PROFILE_CHANGED (core/Database.lua's OnProfileChanged and
ResetAllSpells).
settings/Panel.lua already had the named emitter, Helpers.FireConfigChanged, and
six call sites in settings/Panel_Render.lua already used it — the five outliers
were the ones that had rebuilt it inline. They now call it too. It lives on
NS.Settings.Helpers, which settings/Panel.lua populates as host code whether or
not LibKa0s is present, so the degraded path is unaffected.
core/Database.lua gains a file-local fireProfileChanged(key); both of its paths
call it.
grep -rn 'SendMessage("Ka0s_KickCD_' core modules settings now returns exactly
one line for each of the two messages.
docs/ARCHITECTURE.md's bus catalog and docs/message-bus.md's table both named
the old multi-sender lists; both now name the one emitter and list its callers.
message-bus.md's "adding a message" rule said a second emitter merely needs a
recorded justification — it now says to add a named emitter instead, and states
honestly that SPELL_STATE (three sites) and GRID_LAYOUT (two) remain multi-site
inside their single owning module, which is what the rule actually requires.
Closes KCD-A-08 (cluster C17/architecture-§4 — the bus catalog matches the code).
Verification: luacheck . 0/0 in 32 files; lua5.1 tests/run.lua 749 passed, 0
failed; lizard 0 warnings; git status shows only the seven intended paths.
Mutation: deleting the one SendMessage line inside Helpers.FireConfigChanged
turned tests/run.lua red (748 passed, 1 failed) — the routed paths really do
depend on that single seam. Reverted; 749/0 again. A throwaway probe suite (added
and removed, not committed) also confirmed NS.Settings.Helpers.FireConfigChanged
is a live function on a fully-enabled instance and dispatches to a bus target.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
commit c7c2052aa052b03ed4c360f321743758334dfbb6
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Wed Aug 5 14:54:24 2026 +0530
docs(i18n): sweep the British spellings out of the CCN-elimination plan [M4-28]
docs/superpowers/plans/2026-08-04-ccn-elimination.md is KickCD's own plan file,
not a frozen audit or review bundle, so localization-§5's US-English rule binds
it. Nine lines carried "colour"/"colours"/"Colour"/"Colours" — :51, :53, :57,
:117, :121, :123, :147, :149, :213, exactly the set the finding names (the audit
bundle had listed only five of them). All nine are now the US form; no other
British term appears in the file.
Closes KCD-A-16 (cluster C27 — US English is fixed at the source).
Verification, all with the GFM backslashes stripped from the pattern:
- BEFORE: grep -cniE "colour|behaviour|grey|synthesise|normalis|recognis" <file>
-> 9, on those nine lines.
- AFTER: -> 0.
- MUTATION: planted "(this documents the behaviour, in grey)" into :121 -> the
same grep went to 1 and named the planted line, proving the check fires on the
terms beyond "colour" too. Reverted -> back to 0.
luacheck . 0/0 in 32 files; lua5.1 tests/run.lua 749 passed, 0 failed; lizard 0
warnings; git status shows only the plan file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
commit e3be596230716990d08fbf36cd9f93909d9142dc
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Wed Aug 5 14:52:41 2026 +0530
docs(readme): drop the angle-bracket placeholder from the Troubleshooting row [M4-08]
README.md:191 rendered `/kcd reset <setting>`; the Slash-commands table at :75
already uses the bare `/kcd reset setting` form, so the one row that kept the
angle brackets was the only place a reader was shown a syntax the rest of the
document does not use.
Closes KCD-A-07 (cluster C5 — the root doc set and the README structure are
canonical).
Verification: `grep -n '<setting>' README.md` empty; luacheck . 0/0 in 32 files;
lua5.1 tests/run.lua 749 passed, 0 failed; lizard 0 warnings; git status shows
only README.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
commit 370cebdac4d1b6c3de459e61a7ded9635e66aeca
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Wed Aug 5 14:51:13 2026 +0530
docs(comments): correct the two comments whose count and citation do not resolve [M4-10]
core/PerfSetup.lua's bracket-declaration note claimed "All four of PollSpell's
exits are instrumented now". PollSpell has TWO exits — the `return nil` guard
and the `return state` tail — which is what modules/Cooldowns.lua's own header
over the function has always said. The count now agrees with the code.
settings/Slash.lua and tests/test_coresetup.lua both cited the sibling
AbsorbTracker stub as "(settings/Slash.lua:383)". Read from this repo the path
names THIS file, which is 347 lines, so the citation resolved to nothing; and
AbsorbTracker's own line 383 is inside its profile dispatcher, not the stub. The
citation now names the sibling path (../AbsorbTracker/settings/Slash.lua) and the
symbol (`missing`), both of which resolve, and drops the volatile line number.
Closes KCD-R-09, KCD-R-10 (cluster C3 — a comment's named file or caller exists).
Verification: `ls ../AbsorbTracker/settings/Slash.lua` resolves and `missing` has
one definition there; luacheck . 0/0 in 32 files; lua5.1 tests/run.lua 749
passed, 0 failed; lizard 0 warnings; git status shows only the three paths.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
commit cca553a0e5ed9498dc19d6e6677cde40fc9844f5
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Wed Aug 5 14:49:03 2026 +0530
refactor(exports): drop the unread NS.Castbar export and the dead lint allowlist entry [M4-07]
modules/Castbar.lua published `NS.Castbar = Castbar` with no reader anywhere in
core/, modules/, settings/ or tests/ — the module is reached through Ace3's
`self:GetModule("Castbar")` (core/KickCD.lua:210) and never off the namespace.
Removed.
.luacheckrc read_globals allowlisted `InterfaceOptionsFrame_OpenToCategory`, an
API removed in 10.0. The addon opens its panel with `Settings.OpenToCategory`
(core/KickCD.lua), so the entry allowlisted a symbol no linted file mentions.
Removed.
Closes KCD-R-11, KCD-R-12 (cluster C7 — an exported symbol has a caller or a
documented reason).
Verification: luacheck . 0 warnings / 0 errors in 32 files; lua5.1 tests/run.lua
749 passed, 0 failed; lizard 0 warnings; git status shows only the two paths.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
commit 136b3740ad0a8ae368d1df0ef39a657d6a738787
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Wed Aug 5 14:38:22 2026 +0530
refactor(options): read every layout constant and shared member off the instance [M3-13]
options-ui-§8 and options-ui-§1: a host MUST NOT restate the options library's
layout constants or keep its own copy of a published member. KickCD did both,
and settings/Panel.lua stated the rule in a comment and broke it twenty lines
below the comment.
CONSTANTS (KCD-A-03)
- core/Constants.lua: `Const.PANEL_PADDING_X = 16` deleted, restating
lib.LAYOUT.PADDING_X. The file now carries a comment saying why it is absent.
- settings/Panel.lua: `local ROW_VSPACER = 8` deleted along with the
`Helpers.ROW_VSPACER = ROW_VSPACER` line that ASSIGNED IT OVER the library's
published O.ROW_VSPACER — so the library's value could not have won even
where a caller correctly read the instance.
- tests/test_constants.lua drops PANEL_PADDING_X from the positive-number list.
- docs/module-map.md drops it from core/Constants.lua's constant list.
MEMBERS (KCD-A-02) — three of the four cited host copies dropped, each verified
equivalent to the library member it shadowed before deletion:
- attachTooltip / Helpers.AttachTooltip -> O.AttachTooltip (OptionsWidgets)
- ensureScroll / Helpers.EnsureScroll -> O.EnsureScroll (Options)
- addSpacer / Helpers.AddSpacer -> O.AddSpacer (OptionsWidgets)
- addBlock, a fifth copy of the same body with a return the three call sites
ignore, also dropped; O.AddSpacer already returns the widget.
Pixel-neutral: the library's EnsureScroll applies the identical inset from
lib.LAYOUT.PADDING_X (16), and O.ROW_VSPACER is the same 8.
NEW CASE, tests/test_constants.lua: "no host copy of a LibKa0s-Options layout
constant" — source-scans core/Constants.lua and the three settings/Panel*
files for a numeric-literal assignment to any of the four published names.
Source-scanned per testing-§11 because a reintroduced copy is a DECLARATION:
while the two copies agree, no runtime observation can tell "read the
library's 8" from "read a host 8", which is the entire failure mode.
RED/GREEN: restored both host copies -> FAIL naming
core/Constants.lua:158 and settings/Panel.lua:358 (expected 0, got 2),
748 passed / 1 failed; reverted -> 749 passed / 0 failed. Confirmed the
compliant read shape (`local ROW_VSPACER = Helpers.ROW_VSPACER`,
settings/Panel_Render.lua:20) does not trip it.
SURFACE PARITY. M3-04's gate went red the moment the host copy was deleted:
ROW_VSPACER was present on the degraded path only because Panel.lua published
it unconditionally — parity held for the wrong reason, with the host copy
filling the degraded hole. Adding it to the stub is forbidden (options-ui-§1;
tests/test_options_panel.lua:425 already fails any `ROW_VSPACER =` there), so
it joins PADDING_X / SECTION_HEADING_H / BUTTON_PAIR_REL in
tests/test_surface_parity.lua's live-only exclusion list, with the reason
recorded. On the degraded path it is nil, bound into a local that is forwarded
to a no-op AddSpacer.
TWO DEVIATIONS FROM THE PLAN'S LETTER, both because the plan is wrong about a
measured fact:
1. M3-13 and 03_SPEC.md C21 both require "settings/Panel.lua:307 reads
O.PADDING_X". It cannot: PADDING_X's ONLY consumer in this file was the
host `ensureScroll` copy that the SAME work item deletes (lines 363-364,
the scroll insets). Keeping a read with no consumer is an unused local and
luacheck fails it. Deleting Const.PANEL_PADDING_X outright is strictly
stronger than the acceptance asked for — no host copy AND no host read —
and the guard case above pins it.
2. The fourth cited host copy, `Helpers.LSMValues` (settings/Panel.lua:281,
the one line number that had not drifted), is NOT dropped, and the plan's
own adversarial note already flags this finding's sharpest sub-claim as
wrong. Three measured blockers:
- O.LSMValues returns a DEFERRED CLOSURE; the host's returns a TABLE. The
eight call sites wrap in a closure, so substituting would hand the widget
a function that returns a function.
- The library falls back to STRINGS.LSM_NONE ("None"); the host uses
"Default". That is a user-visible change, and M3-13's own acceptance is
that nothing moves.
- Load order: settings/Icons.lua and settings/Castbar.lua evaluate
H.LSMValues(...) inside schema-row literals AT FILE LOAD. As an instance
member the LibKa0s-absent stub must publish it or those page files raise,
the rows never register, and NS.Settings.Schema loses a large part of
itself — silently taking /kcd list|get|set|reset and the profile defaults
with it. settings/OptionsSetup.lua:140-167 records this as measured, and
tests/test_options_panel.lua pins it.
Recorded instead as a third `## Documented deviations` row against
options-ui-§1, with a re-check trigger that names the condition ending it:
any KickCD page file stops evaluating these at file load.
Closes KCD-A-03 and the three tractable quarters of KCD-A-02; the fourth is
ratified rather than left open.
Verified: luacheck 0/0; tests 749 passed 0 failed (was 748, +1 as designed);
lizard Warning cnt 0; docs/test-cases.md regenerated and diffs clean
CR-normalized; §N.M five-path sweep still 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
commit 6862565c3a4fb1ab37e91f06f66ba777868aa8ed
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Wed Aug 5 14:23:47 2026 +0530
fix(perf): close the two leaked bracket exits, and pin that none can leak again [M3-12]
Two Perf brackets opened and returned without closing, so their buckets
reported a `calls` count short by exactly the number of times the leaked exit
was taken — which reads as a cheap path, not as a bug:
- modules/IconGrid_Render.lua `_tickAllTextIcons` — the empty-set guard
returned unclosed. That exit is taken on the LAST tick of every cooldown
burst, so `cdText` under-counted by one call per burst and dropped the
ticker-teardown cost entirely.
- modules/Castbar.lua `onUpdate` — the no-duration guard returned unclosed.
That exit is taken once per cast, on the frame that tears the OnUpdate
handler down, so `castTick` under-counted by one call per cast.
Both now close on the shape modules/Cooldowns.lua:190-202 already used
(`if __t0 then Perf.Note(<bucket>, debugprofilestop() - __t0) end` before the
`return`), which is where that comment's claim that "tests/test_perfsetup.lua
pins that no exit is left unclosed" came from — it pinned PollSpell only.
NEW CASE, tests/test_perfsetup.lua: "no bracketed function leaks an exit —
every return closes the bracket". It walks every top-level function in the
four bracketed modules, and for any that opens `__t0` requires that every
`return` be preceded by a Perf.Note and that the function close its bracket
at least once.
These are Note-shaped, not Open/Close-shaped, so LibKa0s CANNOT detect them
— there is no stack to leave unbalanced (03_SPEC.md C9 says so explicitly).
Behavioral coverage is unavailable too: `cdText` is driven by a C_Timer
ticker and `castTick` by an OnUpdate script, neither of which the harness
drives — which is precisely why these two survived a suite that already had a
hand-written four-exit case for PollSpell. testing-§11's source-scan shape is
the correct fallback and the case comment carries its justification.
RED/GREEN, both arms of the new case:
plant 1 — revert both fixes (the exact pre-M3-12 state):
FAIL ... IconGrid_Render.lua:826 _tickAllTextIcons — unclosed `return`
Castbar.lua:689 onUpdate — unclosed `return` (expected 0, got 2)
747 passed, 1 failed
revert -> 748 passed, 0 failed
plant 2 — delete every cdText Note so the bracket is never closed:
FAIL ... unclosed `return` AND "opens a bracket it never closes"
746 passed, 2 failed
revert -> 748 passed, 0 failed
Closes KCD-R-05, KCD-A-18.
Verified: luacheck 0/0; tests 748 passed 0 failed (was 747, +1 as designed);
lizard Warning cnt 0; docs/test-cases.md regenerated and diffs clean against
`lua5.1 tests/run.lua --list` with both sides CR-normalized.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
commit 2742a95fa0e56d5db18411f3859c261dc9394f02
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Wed Aug 5 14:16:18 2026 +0530
docs(deviations): give KickCD's register the mandated shape and one home [M3-08]
documentation-§3 makes `## Documented deviations` the ninth mandated section
of docs/ARCHITECTURE.md, with the exact row shape
`| Rule | What differs | Why | Decided | Re-check trigger |`, and makes it
the SINGLE home for a ratified deviation. KickCD's register was six
"Deviation recorded as intentional" paragraphs scattered across four topic
docs in an ad-hoc shape, with no rule citation, no decision date and no
re-check trigger — which is exactly the state that gets a ratified decision
re-filed as an open MUST failure every audit cycle.
Added the section after `## Known limitations` with two rows, both against
`savedvariables-§1`:
- the two profile migrators that run off the stored SHAPE rather than off
db.global.schemaVersion (reasoned at docs/saved-variables.md:212)
- the DEFAULT_PROFILE restructure that nested target/focus under
units.<unit> (reasoned at docs/saved-variables.md:222)
Decided dates are the dates the decisions were actually recorded, recovered
with `git log -S` (2026-07-16 and 2026-07-15) rather than backdated to today.
Both re-check triggers name a condition that ENDS the deviation and can be
observed to have fired.
The two source paragraphs now say the register is the record and they are the
reasoning, per the section's "MAY be reasoned at length elsewhere" clause.
FOUR OTHER RECORDS DELIBERATELY NOT ROWED, each named in the section with the
reason, so a later audit does not read the omission as an oversight:
- the two KickCD<Widget><UnitTitleCase> frame-naming notes
(docs/conventions.md:19-20) and the additive GRID_LAYOUT payload note
(docs/message-bus.md:49) depart from THIS ADDON'S conventions, not from a
numbered rule; naming-cheatsheet has no subsections, so there is no
filename-§N to cite and a register of standards deviations is the wrong
home for them.
- the Ka0s_KickCD_PROFILE_CHANGED two-emitter note
(docs/ARCHITECTURE.md:79) IS a real architecture-§4 deviation, but it is
being FIXED, not ratified: M4-04 reduces both CONFIG_CHANGED and
PROFILE_CHANGED to one sender. Rowing it here would have created a
register entry that M4-04 then has to retire in the same milestone.
Verified: grep -n "Documented deviations" docs/ARCHITECTURE.md resolves;
both rows match ^| `<filename>-§N` | … | YYYY-MM-DD | …; no §N.M
reintroduced. luacheck 0/0; tests 747 passed 0 failed; lizard Warning cnt 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
commit e8408760dee677673689604ad979daab1b780e10
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Wed Aug 5 14:12:00 2026 +0530
docs(refs): retire the global §N.M notation for filename-§N [M3-07]
Comment-only. Every retired global `§N.M` reference in KickCD's code, tests,
docs and config is rewritten to the `filename-§N` scheme
(documentation-§6). 36 sites across 22 files, plus the regenerated
docs/test-cases.md, whose case titles carry six of them.
The mapping was derived from the pre-split standard
(WowAddonStandards 070caf2^:standards/01_STANDARD.md) and then RANGE-CHECKED
against the current section files, not assumed:
§1.2 Tier 2 — Modular (1500 LOC cap) -> layout-§1
§2.2 SavedVariables naming -> toc-file-§2
§3.4 Lib registry pattern -> library-stack-§4
§4.1 Namespace bootstrap -> architecture-§1
§4.4 Closed message bus -> architecture-§4
§5.1 AceDB structure -> savedvariables-§1
§7.4 Help output & chat tag -> slash-commands-§4
§12.2 Monospace font (shipped) -> debug-logging-§2
§12.3 Line format -> debug-logging-§3
§12.4 The sink -> debug-logging-§4
§12.5 Enabled-state, session-only -> debug-logging-§5
Only layout renumbered in the v1.5.0 split (the three Tier-1/Tier-2/Casing
subsections collapsed to one "Modular layout"); every other subsection kept
its ordinal, which is why the mapping looks like an identity and had to be
checked rather than trusted.
Swept with ALL FIVE exclusions — libs/, tests/_kit/, docs/audits/,
docs/reviews/ AND docs/automated-tests/. KickCD is the repo where the fifth
one decides the outcome.
Closes the KickCD row of the C15 cluster (AT-A-11, BL-A-11, CM-A-20,
LH-A-19, WG-A-15 are the sibling repos' ids).
Verified:
five-path exclusion -> 0 hits (was 41)
four-path exclusion -> 30 hits, ALL of them inside the three frozen
automated-test bundles 20260804-182144/, -214315/, -233245/
(test-cases.md + tests.txt), exactly the 30 the plan predicted would
and must survive as evidence.
Every remaining filename-§N reference in the repo range-checks against
its section file's local heading count.
docs/test-cases.md regenerated; `lua5.1 tests/run.lua --list` diffs clean
against it with both sides CR-normalized.
luacheck 0/0; tests 747 passed 0 failed; lizard Warning cnt 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
commit 1117274b218c94645dfdf10c0d4f4b6637d7e70f
Author: Tushar Saxena <tushar.saxena@gmail.com>
Date: Wed Aug 5 14:06:32 2026 +0530
docs(gates): name the checkpoint in every gate statement [M3-06]
Sweep 3f. A bare "Gates? yes/no" column cannot be written honestly: there
are two checkpoints — the run/commit and the tag — and perf and complexity
answer differently at each. Every hand-written gate statement now names both.
- docs/testing.md: the Gates? table splits into "Gates the run and the
commit?" and "Gates the tag?"; the paragraph below it states that perf
and complexity never fail a run and never block a commit, but DO gate the
tag (all four at pass plus zero functions above CCN 15, evaluated by
/wow-addon:bump-version from the release run's manifest.json); the "At
release, not at commit" paragraph says what the tag is gated on.
- docs/automated-tests/README.md: same split under the section heading,
renamed "What gates at which checkpoint"; the skip sentence now says a
skip is NOT EVALUATED at the release gate rather than passed.
- CLAUDE.md: "Complexity — recorded, never a gate" becomes "recorded at the
commit, gating at the tag".
- docs/automated-tests/RESULTS.md: the lead-in is replaced with the text the
re-vendored v1.8.0 runner emits verbatim. Every past row is untouched.
PLAN CORRECTION. M3-06 says "The RESULTS.md lead-in comes from the
re-vendored runner, not from this sweep." It does not. The runner writes
that block only in the branch that CREATES RESULTS.md
(tests/_kit/run-automated-tests.sh:414-433); when the file exists and its
header matches, it inserts the row and nothing else (:403-406). No runner
invocation would ever have replaced the stale lead-in in this repo, so the
acceptance grep was unreachable without a hand edit. The replacement text is
copied byte-for-byte from the runner's own printf block, so the next fresh
bundle in a new repo produces the identical wording.
Closes KCD-A-13 (the C8 cluster's KickCD row).
Verified: grep -rn "never fail a run" docs/ CLAUDE.md returns only
checkpoint-qualified sentences plus the frozen docs/audits/ evidence line,
which is untouched by design. luacheck 0/0; tests 747 passed 0 failed;
lizard Warning cnt 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This mod has no additional files

