v0.6.0
What's new
Vanguard
v0.6.0 (2026-10-14)
- Merge pull request #347 from clifftaylor2325/work/release-0.6.0
release: v0.6.0 - Merge main into release-0.6.0
- Merge pull request #386 from clifftaylor2325/work/release-0.6.0-docs
0.6.0 release prep: full notes, changelog and docs split by reader - #377 nameplates: friendly switches use the client's real settings (#387)
Cliff's probe in game: nameplateShowFriends is nil on the Forever
client, nameplateShowFriendlyPlayers is 1 and nameplateShowFriendlyNPCs
is 0, so the Friendly switch read off and a click set nothing. The
Nameplates page now has Friendly players' and Friendly NPCs' switches on
those two settings, and /vg plates reports both.
Tests: plates.test.mjs report and switch cases moved to the two
settings; Nameplates page row count 14 to 15. README, smoke step 7.5
and the 0.6.0 What's new line follow.
Verified: npm run quality, npm run coverage (100%). luacheck not
installed. CI skipped at Cliff's request. - release prep 0.6.0: memory, nameplate switches and tracker Show all in the notes
- release prep 0.6.0: notes, changelog, docs split by reader
What's new notes cover the quest, trainer, weights, prep, plates, windows and meter work merged since the first draft. docs/ gains GETTING-STARTED, COMMANDS, CHANGELOG and an index by reader; finished hand-offs (crest, click casting, prompt template) are removed. A test checks COMMANDS.md against the README. - Merge pull request #385 from clifftaylor2325/work/quest-tracker-height
Quest tracker: remember Show all, and stop at 10 quests - Merge pull request #384 from clifftaylor2325/work/nameplate-cvar-rows
#377 nameplates: Enemy and Friendly plate switches, enemy plates on by default - Quest tracker: remember Show all, and stop at 10 quests
Cliff chose to keep the expanded tracker across reloads, and found the
expanded list runs into the bars with 14 watched. The expanded state now lives
in the QuestTracker profile settings (expanded, default false), and the tracker
draws at most QT.MAX_SHOWN = 10 quests; past that the line reads
"Show fewer ยท +N more". Tests cover the saved choice, a login that starts
expanded and the cap. README and smoke step 8.4 updated. quality and
coverage (100%) pass. - #377 nameplates: Enemy and Friendly plate switches, enemy plates on by default
The Nameplates page in /vg now has the game's own Enemy nameplates (V)
and Friendly nameplates (Shift-V) switches, read from and written to
nameplateShowEnemies and nameplateShowFriends, applied out of combat.
Vanguard turns enemy plates on once per character (VanguardCharDB
.enemyPlatesSet), since the game ships them off and Vanguard then has
nothing to dress. After that the player's V key or the switch decides;
a reload never turns them back on. A client without SetCVar changes
nothing. The /vg plates hint now points at the Nameplates page.
Tests: plates.test.mjs covers the two switches (in and out of combat),
the once-per-character default and a client without SetCVar; the
options row count for Nameplates goes 12 to 14. README nameplates
paragraph, smoke step 7.5 and a 0.6.0 What's new line updated.
Verified: npm run quality, npm run coverage (100%). luacheck not installed. - Merge pull request #383 from clifftaylor2325/work/memory-footprint
Memory: 7-day price history, one gear link per item per scan, bounded chat alerts - Memory: 7-day price history, one gear link per item per scan, bounded chat alerts
Cut what Vanguard holds after an auction house scan, found with a table
census of the headless client and real Lua 5.1 size checks:- AP.DAYS 30 -> 7: every reader (Low7, Market7, LowBefore) looks at the last
7 days, so older daily values were dead weight. A full 6000-item house goes
from about 25.6 MB to 9.1 MB. Prices still expire after 30 days. - A scan keeps the cheapest link per item (AH.GearKey) instead of one per
listing (each carries a unique ID). Peak scan state in a 40,000-listing
model: 5.9 MB -> 3.5 MB. Same result: cheapest per item, smaller link on a
tie. The judging walker indexes the sorted links instead of one table each,
and GearKey builds no table. - ChatWatch.recent sweeps alerts older than the repeat window once 100 are held.
Verified: quality and coverage (100%). Needs an in-game /vg mem check.
- AP.DAYS 30 -> 7: every reader (Low7, Market7, LowBefore) looks at the last
- Merge pull request #382 from clifftaylor2325/work/nameplate-enemies
#377 nameplates: /vg plates reports and sets the game's enemy plate setting - Merge pull request #381 from clifftaylor2325/work/mail-tools-fix
Mail tools: hide unset presets, say when empty, sit on the mail window's layer - #377 nameplates: /vg plates reports and sets the game's enemy plate setting
Vanguard's nameplate module has no enemy filter and never touches the
plate CVars: it dresses every plate NAME_PLATE_UNIT_ADDED hands it. A
hostile unit with only the game's small red name and no plate means the
client made no plate, which is what the game's Enemy nameplates setting
(nameplateShowEnemies, the V key) being off looks like.
/vg plates now prints how many plates Vanguard drew, how many are
attackable, and the game's Enemy, Friendly and Show all plate settings,
with a hint when Enemy is off. /vg plates enemies on|off sets it, after
the fight when in combat.
Added: plates.test.mjs case for the report, the set, and the in-combat
deferral. README nameplates paragraph and command list, /vg help, smoke
step 7.5 (7.5/7.6 renumbered to 7.6/7.7).
Verified: npm run quality, npm run coverage (100%). luacheck not installed. - mail tools: hide unset presets, say when empty, sit on the mail window's layer
- The panel can first be built by MAIL_INBOX_UPDATE (the game shows MailFrame after
MAIL_SHOW), and then PaintPresets never ran, leaving four buttons reading "Preset".
Paint now labels and hides the preset buttons every time it draws. - An empty inbox or a search with no match showed a blank panel; it now says
"Your inbox is empty." or "No mail matches.", and a hint says how to add a preset
while there are none. - The panel was 94% opaque on UIParent's layer, so a HUD frame behind it showed
through (the dim "18 Korwynn" row in Cliff's shot). It is now opaque, on the mail
window's strata and level, and placed right of the window's padded panel. - README sentence updated.
Verified: npm run quality, npm run coverage (100%), new mailtools scenario.
Needs in-game check: the panel's look and placement beside the mailbox.
- The panel can first be built by MAIL_INBOX_UPDATE (the game shows MailFrame after
- Merge pull request #380 from clifftaylor2325/work/quest-log-watch-list
Quest log: read tracking from the watch list; Shift-click tracks - Merge pull request #379 from clifftaylor2325/work/quest-header-chevrons
Windows map: quest zone headers get a chevron for the game's yellow bar - Quest log: read tracking from the watch list; Shift-click tracks
Cliff reports Track/Untrack does nothing. The log decided "watched" from
C_QuestLog.GetQuestWatchType(id) ~= nil. If the client returns a value (0 or
false) for an unwatched quest, every quest reads as watched: the button always
says Untrack and each click calls RemoveQuestWatch on a quest that isn't watched,
so nothing happens. The log now asks the same watch list the tracker draws from
(GetNumQuestWatches / GetQuestIDForQuestWatchIndex), which visibly works.
Also: Shift-click a quest line tracks or untracks it, and when the game leaves
the watch unchanged (its watch limit, say) chat says so with the count tracked.
The test stub now returns 0 from GetQuestWatchType for every quest; the old code
fails it. README and smoke step 8.3 updated. quality and coverage (100%) pass. - Windows map: quest zone headers get a chevron for the game's yellow bar
From /vg frame on the live list, each zone header is a button with ButtonText, a
full-width collapseExpand atlas texture (the yellow bar) and a CollapseButton. With
Vanguard's quest log off, that art is faded and a gold Down or Right chevron sits
over the collapse button, from the entry's isCollapsed. Headers are dressed when the
list shows and after each QuestLogQuests_Update, since the game reuses them.
Verified: windows-map test, npm run quality, npm run coverage (100%). - Merge pull request #378 from clifftaylor2325/work/quest-log-track
Quest log: Track and Untrack redraw the button at once - Merge pull request #376 from clifftaylor2325/work/weights-exact-box
Upgrades: exact-value box beside each weights wizard slider - Quest log: Track and Untrack redraw the button at once
The Track/Untrack button read the watch state once, when the quest was chosen,
and only redrew on QUEST_LOG_UPDATE. A watch change fires QUEST_WATCH_LIST_CHANGED,
which the log did not listen to, so the label stayed stale and the next click
repeated the same call (Untrack again, never Track).
Now the button redraws straight after the call from what the game reports, and
the log also redraws on QUEST_WATCH_LIST_CHANGED (a right-click in the tracker).
The test stub now keeps watch state, so the label flip is asserted.
Not changed: the watch cap; if Track still does nothing at the cap, that is the game refusing the call. - Upgrades: exact-value box beside each weights wizard slider
Per Cliff, each slider in the stat weights wizard has a number box.
It shows the weight as the slider moves; typing a number and pressing
Enter (or leaving the box) sets it exactly, past the slider's 0 to 3
range too. Anything that isn't a number puts the weight back.
README and smoke test 31.2 updated. Verified: npm run quality clean,
npm run coverage 1441 tests passing, 100% Lua line coverage.
Needs an in-game check. - Merge pull request #370 from clifftaylor2325/work/stat-weights-wizard
Upgrades: stat weights wizard - Merge pull request #371 from clifftaylor2325/work/auction-post-error-10
AuctionSell: Post names the auction house's error code - Merge pull request #363 from clifftaylor2325/work/group-board-empty
Group board: show what chat it hears, add /vg lfg probe - Merge pull request #375 from clifftaylor2325/work/meter-single-line
meter: fill the top bar when its figure is under 1 - Options test: Town and gear row count 49 after merging main
main added the Instance prep setup row; this PR adds Set up stat weights. - Merge pull request #374 from clifftaylor2325/work/clock-by-date
Info strip: put the time beside the date - meter: fill the top bar when its figure is under 1
The bars' scale had a floor of 1, so a lone line with 0.31 healing per second
drew its bar a third full. The scale is now the top line's own figure (1 only
when it is not above 0), so the top bar is always full.
Verified: meter test gains a 0.31 case; test:quick, quality and coverage pass. - Merge remote-tracking branch 'origin/main' into work/stat-weights-wizard
Conflicts:
Core/Slash.lua
- Merge pull request #373 from clifftaylor2325/work/auction-lists-bugs
auction: Lists tab Deals market, Deals name width, Upgrades duplicates - Merge pull request #372 from clifftaylor2325/work/prep-wizard
Instance prep: wanted-amounts window - groupboard: probe quotes the last line that was not a post
Cliff's Trade lines ("LFG HoT - Can tank or DPS") parse as an LFG post
with no dungeon and pass the level filter in the harness, so the parser
is not what hides them. The probe now quotes the last line that was read
but not taken as a post, to tell a parse miss from a board that hears
nothing.
Verified: groupboard test with those exact lines, quality, coverage 100%.
Needs an in-game check. - Upgrades: wizard slider refresh no longer shadows refresh()
Renames ownRefresh's parameter so luacheck's shadowing warning (CI Quick checks) clears. Verified: upgrades tests pass, npm run quality clean. - Upgrades: stat weights wizard covers every stat, no role page
Per Cliff's steer, the wizard is one panel: a scrolling list with a
slider for each of the 21 stats, for the weights in use (the kind
the class and the existing Weigh stats for choice make). The stats
that kind values come first, then the rest by name. Defaults, Save
and Cancel as before; the role page and review page are gone, and
UP.Role is back to its original shape.
Plain /vg weights with Gear upgrades off now says the module is off
instead of erroring.
Verified: npm run quality clean, npm run coverage 1437 tests
passing, 100% Lua line coverage. Needs an in-game check. - Info strip: put the time beside the date
The time moved from the gold row to the date row, followed by a shorter
date ("21:04 Tue 13 Oct"); the calendar button stays at the row's right
end. With the clock off the date takes the row's left edge. Fps and
latency are unchanged. README, smoke test and strips test updated.
Verified: npm run quality, npm run coverage (100%). - auction: Lists tab Deals market, Deals name width, Upgrades duplicates
Deals judges against the median of the last 7 days' market values, and only
when scans priced the item on at least 2 of those days, so one odd day cannot
set a -98 % deal. The Deals columns are narrower and the judged-against label
shorter ("Prior 6-day low") so the Item column gets room. Upgrades keys gear
by item, enchant, gems and suffix without the per-listing unique ID, so the
same item seen twice keeps its cheapest listing.
Verified: auction tests, npm run quality, npm run coverage (100 %). - Instance prep: wanted-amounts window
A three-step window (supplies, class supply for hunters and warlocks, review)
to set how many food, bandages, potions, ammo and soul shards to carry, with
Use suggested by level. Opened by a Set wanted amounts button under Instance
prep and by /vg prep setup; /vg prep want still works. README, /vg help and
smoke test 6.5b updated.
Verified: npm run quality and npm run coverage (1439 tests, 100% floor) pass.
Needs an in-game check. - AuctionSell: Post names the auction house's error code
Error 10 in chat was a bare number. It is Enum.AuctionHouseError.DatabaseError
in Blizzard's API docs; chat now says the client's own name next to the code
("error 10 (DatabaseError)"). The Post arguments were checked against the
documented PostItem/PostCommodity signatures and match (location, length index
1-3, quantity, bid/buyout or unit price), so no call change is made.
Verified: auctionpost test, quality, coverage 100%. - Upgrades: stat weights wizard
A three-page wizard for Gear upgrades' stat weights, opened with
/vg weights wizard or Set up stat weights under Town and gear:
the role (with the kind of character it makes for your class),
sliders for the seven stats the kind weighs most with Defaults,
and a review of every weight. Save stores the role and only the
weights that differ from the defaults; Cancel changes nothing.
/vg weights set, reset, export and import are unchanged.
UP.Role takes an optional role so the wizard can resolve "auto"
before saving. /vg weights wizard opens before the weights are
read, so with the module off it says so instead of erroring.
README, /vg help and smoke test 31.2 updated. Tests in
upgrades.test.mjs; options row count for Town and gear 47 -> 48.
Verified: npm run quality clean, npm run coverage 1437 tests
passing, 100% Lua line coverage. Needs an in-game check. - groupboard: say what chat the board hears, add /vg lfg probe
The board showed "0 posts" and gave no way to tell a quiet chat from a
board that hears nothing. The capture and parse code reads correctly
(CHAT_MSG_CHANNEL/YELL/SAY/GUILD through ns.On, text then sender, secret
and own lines skipped, the level filter lets posts that name no dungeon
through), so nothing in it is provably wrong.
Now every chat line is counted by why it was skipped (channel off,
hidden by the game, missing, your own) or kept. The empty board's note
shows the counts, and /vg lfg probe also lists the channels you are in
(GetChannelList), the channels read and the filter state.
Verified: groupboard test (new probe test), npm run quality, npm run
coverage (100%). Needs an in-game check. - Merge pull request #362 from clifftaylor2325/claude/project-thread-3q3o0w
Theme: remove the Frost variant - Theme: remove the Frost variant
Cliff found the light look too bright even after the pale slate tone, so
Frost is gone: its token set, the light accent-text variant
(T.ACCENT_VARIANTS_LIGHT and the per-set cache key), its entry in
VARIANT_ORDER, the options dropdown, /vg variant's help and usage, the
0.6.0 What's new note, README, THEME.md and the smoke test. The variants
are the eight dark looks again.
A profile that saved variant = "frost" (or any name no longer in
T.VARIANTS) migrates back to the default look in the theme migration, so
nothing shows a stale choice.
Verified: npm run quality and npm run coverage (100% floor, 1435 tests).
theme.test.mjs checks the saved-frost migration and that /vg variant frost
is refused; the "recolours text drawn before it" test now switches to
Classic look instead. Needs an in-game check that a character left on
Frost loads on Slate and brass with no error. - Merge pull request #361 from clifftaylor2325/claude/project-thread-3q3o0w
Theme: variant switch recolours existing text, Frost toned down, crisp segment text, segments sized to labels - Theme: Frost inset hex that typos reads as a word
d5dbe7 tripped the typos check (dbe โ be); d4dae6 is the same pale slate
one step darker and clears it. Theme.lua and THEME.md.
Verified: theme and options tests pass. - Theme: Frost toned down, crisp text on accent fills, segments sized to their labels
Frost's panels were pure white; they are now pale slate (e4e8f0 family) with a
slightly darker inset and raised tier, no text shadow and no sheen, and the
state, reaction and accent colours deepened to keep 4.5:1 on the lighter
panels.
Dark "void" text on an accent fill (a selected choice segment, a filled
button) was blurred by the theme's text shadow. T.Color now clears the shadow
under void text (a new "clear" token) and puts the theme's shadow back when
the text is recoloured by another token; T.RefreshText does the same on a
variant switch.
Choice segments were a fixed 44 px, so a long label or a larger text size ran
past its segment. Each segment is now sized from its label's string width plus
8 px of air each side, laid out right to left along the row's edge, and
re-measured on every refresh so a font or text size change follows.
Verified: npm run quality, npm run coverage (100% floor, 1435 tests). New
options test covers segment widths, the layout after a refresh and the shadow
under the selected segment; the stub records SetShadowColor. Needs an in-game
check of Frost's tone and of the selected segment's text at 100% and 125%. - Merge pull request #360 from clifftaylor2325/work/trainer-train-all-align
Errands: Train all matches Train's height - Theme: a variant switch recolours text drawn before it
Switching to Frost with the options window open left most of its
labels, dropdown values, the title and the footer in the dark look's
light grey (Cliff's screenshot): T.Color set a token's colour once and
T.RefreshText only re-fonted. The dark variants hid this because all
their text is light.
T.Color now remembers the token per font string in a weak side table
(and forgets it when given r, g, b by hand), and T.RefreshText
recolours every remembered string, so a variant or accent change
reaches text that already exists.
Verified: theme.test.mjs opens the options window, switches to Frost
and checks every shown font string in the window clears 4.5:1 on the
panel (the preview's bar text excepted), and that text coloured by
hand is left alone on the way back. npm run quality and npm run
coverage (100%) pass. Needs an in-game look at Frost with the window
already open. - errands trainer: Train all matches Train's height
In the game Train all was taller than Train beside it, so their top and bottom edges did not line up. It now takes Train's height. Test added. - Merge pull request #358 from clifftaylor2325/work/trainer-money-overlap-2
Errands: narrow Train all so it clears the money line - Merge pull request #359 from clifftaylor2325/claude/project-thread-3q3o0w
options: list Frost in the Theme variant dropdown - options: list Frost in the Theme variant dropdown
PR #355 added the Frost variant to T.VARIANTS and /vg variant but not
to VARIANT_CHOICES, so General -> Look -> Theme variant showed eight
looks and Frost was reachable only from the slash command (Cliff's
screenshot). Added the entry.
Verified: options.test.mjs gains a case that clicks the Theme variant
row once per entry in T.VARIANT_ORDER and expects to visit every one
(Frost included) and come back to the default, and clicks Corners
through rounded and square. npm run quality and npm run coverage
(100%) pass. - errands trainer: narrow Train all so it clears the money line
The first fix anchored Train all to a money frame name that does not exist in the game, so the button still covered the money. Train all is now a fixed 96 wide beside Train, and its label is Train all (N); the cost is reported in chat on click, as before. README and smoke test updated. - Merge pull request #357 from clifftaylor2325/work/clickcast-picker-parent
Click casting: keep the spell picker on UIParent so clicks reach it - click casting: keep the spell picker a child of UIParent
Moving the picker under the Add key button when it opened left it below the
full-screen click catcher in the game: any click on it (a spell row or the
search box) hit the catcher and closed it. The picker now stays on UIParent
like the other dialogs and closes when its anchor hides instead. The strata
fix from the last change is dropped as no longer needed; the test stub's
SetParent strata model stays. - Merge pull request #356 from clifftaylor2325/work/clickcast-key-bind
Click casting: Add key picker stays above its click catcher - Merge pull request #355 from clifftaylor2325/claude/project-thread-3q3o0w
Theme: Frost light variant and a rounded or square corners setting - fix click casting: Add key picker stays above its click catcher
The picker is reparented under the Add key button when it opens. The game
gives a reparented frame its new parent's strata unless the strata is
fixed, which dropped the picker below the full-screen catcher: clicking
Renew hit the catcher, which closed the picker and bound nothing.
The picker now fixes its strata (SetFixedFrameStrata). The test stub now
models the parent's strata on SetParent, and a test checks the picker
keeps the catcher's strata. - errands trainer: rename the money line local so luacheck passes
The Quick check's luacheck step on main warns thatmoneyin
ER.PlaceTrainButton shadows the money() helper (from 74096e1). Renamed
to moneyLine; no behaviour change. main merged in alongside. - Merge remote-tracking branch 'origin/main' into claude/project-thread-3q3o0w
- Theme: name the corner inset offsets so the typo check passes
The Quick check's typos step readiyas a misspelling ofit. No behaviour change. - Theme: Frost light variant and a rounded or square corners setting
Frost is the first light look: white panels, dark blue ink for text
and the border, no sheen. A variant flaggedlighttakes its accent
text shade from T.ACCENT_VARIANTS_LIGHT (darker instead of lighter),
and Frost deepens the state, gold and reaction colours so they read
on white.
The corners setting ("square", the default, or "rounded"; General ->
Look -> Corners, /vg corners) rounds painted panels and buttons on
every theme by T.CORNER_RADIUS (4) screen pixels: the edges stop
short of the corners, four CornerLine arcs take the border colour,
the fill is pulled in with two side strips and four CornerFill
quarter discs, the sheen, bevel and hover lines stop short, and the
panel gradient is skipped. The parts are made the first time a frame
is painted rounded and hidden when it goes back to square. Status
bars, slots and tabs stay square. The two corner textures are new
Media generated from scripts/lib/media-sources.mjs.
Listed in the options, /vg help, the 0.6.0 What's new notes, README,
docs/THEME.md and smoke tests 26.1 and 26.2.
Verified: theme.test.mjs gains a Frost case (every text colour,
accentText included, clears 4.5:1 on white; the contrast helper now
works either way round), a rounded-corners case (art made on demand,
edges and fill pulled in by r, a status bar and a borderless panel,
gradient off, square hides it all) and a High contrast ring case.
media.test.mjs lists the two corner textures as built by name; the
General page row count is 34. npm run quality and npm run coverage
(100%) pass. Needs an in-game look at the arc's crispness at 1440p
and 4K and at every Frost window. - Merge pull request #354 from clifftaylor2325/work/trainer-money-overlap
Errands: keep Train all clear of the money line - Quest tracker: click "+N more" to show every watched quest (#353)
The tracker's body is a fixed 152 px, so with 20 quests watched only 2 fit and the rest were just counted. Clicking the count now grows the tracker down (up to 70% of the screen) and shows them all; "Show fewer" folds it back. Test added; README updated. The 20-quest limit is the game's own watch cap, not ours.
Co-authored-by: clifftaylor2325 319937424+clifftaylor2325@users.noreply.github.com - errands trainer: keep Train all clear of the money line
Train all was a fixed 170 wide, anchored left of Train, so it ran over the player's money at the bottom left. It now also anchors to the right of the trainer's money frame when there is one, so it fills only the space between. Test added; quality and coverage pass. - Merge pull request #352 from clifftaylor2325/claude/project-thread-3q3o0w
Theme: four more theme variants (Midnight, Obsidian, Moss, Ash and ember) - Theme: four more theme variants (Midnight, Obsidian, Moss, Ash and ember)
Adds four dark looks to T.VARIANTS, beside High contrast, Vellum and ink
and Classic look: Midnight (cold navy-black, ice-blue text), Obsidian
(neutral charcoal, light grey text, a silver edge), Moss (dark forest
green, pale sage text) and Ash and ember (smoky grey, a rust edge and
dividers, warm grey text). Each is a token set only; no new art, no
change to the border language.
Listed in the options cycle, /vg variant and its HELP line, the 0.6.0
What's new notes, README, docs/THEME.md and smoke test 26.1.
Verified: theme.test.mjs gains a case that switches through each one
and checks text, text2 and muted clear 4.5:1 over every variant's
panel. npm run quality and npm run coverage (100%) pass. Needs an
in-game look at each variant on the options window, bags, quest log,
a unit frame and a tooltip. - Merge pull request #351 from clifftaylor2325/claude/project-thread-80dxqc
fix meter: start clear of the bags and menu bar - fix meter: start clear of the bags and menu bar
The meter's default spot was 340 in from the right edge, but the bags and
game menu strip is about 345 wide and sits 16 in, so the strip covered the
meter's right edge (Cliff's screenshot). The meter now starts 372 in, a
small gap left of the strip. Only the default changes; a meter already
moved with /vg unlock keeps its place.
Verified: npm run quality and npm run coverage (100%) pass. Not checked in game. - Merge pull request #350 from clifftaylor2325/claude/project-thread-6s659r
Quest tracker: start below the info strip's tallest height - /vg frame: #n names the nth child frame (#349)
An unnamed frame (a quest list header) had no name to dump. QuestScrollFrame.Contents.#3
is now the third child frame of Contents.
Verified: core test, npm run quality, npm run coverage (100%). - Merge pull request #348 from clifftaylor2325/claude/project-thread-fe19wh
setup: role layouts and presets share one set of role settings - Quest tracker: start below the info strip's tallest height
The info strip grows to seven rows (date, leveling and gold rates), but the
tracker's default spot began under four, so the strip covered the first
quest. The spot now starts 60 px lower and is 60 px shorter (174), so it
ends where it did before. Saved mover positions are unchanged.
Verified: test:quick, quality and coverage (100%) pass. - setup: role layouts and presets share one set of role settings
Cliff asked for popular role configurations. ElvUI's installer tells its
roles apart by where the group frames go (healer: bottom centre by the
player; tank and DPS: the screen's left edge) plus heal prediction and raid
debuffs for healers; EllesmereUI's packaged profiles (OakUI) differ between
Healer and Tank/DPS in party frame settings only. Vanguard's default spot
is already the healer-style placement (#331, #335), so the roles differ in
settings; whether Damage should also park the frames at the left is on a
card to Cliff.
Core/PresetStrings.lua now holds ns.ROLE_SETTINGS (Healer, Damage, Tank):
raid frames, raid auras, raid healing and nameplates per role. The Healer
and Damage tables are what setup's Healing and Damage and tanking layouts
apply, and the three preset strings are encoded from the same tables at
load, so the two can't drift. Healer adds raid debuffs (3), HoTs, incoming
heals, shields and the buff watch; Damage and Tank take those off and keep
the aggro strip. No preset sets compact any more: the old Healer string
turned it off, which makes a 40-man overlap on a 1365x768 screen (the
layout test caught it).
Also a What's new line under Setup, README, smoke test 2.3 and the setup
hints.
Verified: npm run quality at baseline, npm run coverage 100% with every
test passing; a new integration test decodes each preset string against
its role table. Needs an in-game check: pick each layout in /vg setup and
look at the Raid frames and Nameplates pages; apply each preset from
Profiles. - release: v0.6.0
- setup: role layouts set raid frame and nameplate settings
The Healing and Damage and tanking layouts in setup only moved frames and
turned modules on, so both looked the same in game: the raid frames sit at
the same default spot either way and only click casting differed. The
settings that tell the roles apart lived only in the Healer and Damage
presets under Profiles, which setup never applied.
Each layout now sets those settings too. Healing: power bars and sort by
role on the raid frames, two debuffs and no threat colours on nameplates.
Damage and tanking: no power bars or role sort, six debuffs and threat
colours. The settings are written through ns.Settings so a module that is
off keeps them for its next load; frames on screen repaint at once and
Done's reload reason names the rest. Raid frame positions are unchanged.
Verified: setup and setuplayouts harness tests (27 passing, three new
assertions cover both layouts and the module-off path), npm run quality
at baseline. Needs an in-game check that the Raid frames and Nameplates
pages show the new values after picking each layout. - docs: add Cliff's commit and PR attribution rule to CLAUDE.md (#346)
docs: add Cliff's commit and PR attribution rule to CLAUDE.md - docs: add Cliff's commit and PR attribution rule to CLAUDE.md
Agents attribute commits only to clifftaylor2325: no Co-Authored-By,
Claude-Session or other author or co-author lines, no other git author
or committer identity, and no 'Generated with Claude Code' or session
links in PR descriptions. This overrides default attribution guidance.
Verified: CLAUDE.md-only change; no code or generated output touched. - Merge pull request #345 from clifftaylor2325/claude/project-thread-0qj9ey
release: publish each tag to CurseForge and Wago - release: send each tag to CurseForge and Wago, and add the project IDs
Vanguard.toc gets X-Curse-Project-ID, X-Wago-ID and the website as X-Website. release.yml gets a listings job after the GitHub release that runs the BigWigs packager (v2.6.1, pinned by commit) when CF_API_KEY and WAGO_API_TOKEN are set. README's Releases section is updated.
Verified: workflow and pkgmeta tests, TOC check, quality scan, test:quick. Not run: the packager itself, which needs a real tag. - Merge pull request #344 from clifftaylor2325/claude/project-thread-l2qvea
brand: the crest in the options window header - brand: the crest in the options window header
T.Logo now draws Media/Textures/Icon (the crest, pre-colored) instead of the two linked diamonds, so the /vg window header matches the addons-list icon. The header mark is 28 px (was 22). Also comments media.test.mjs near(), which the quality scan flagged on main.
Verified: npm run quality, npm run coverage (100% floor), build-media --check. Not seen in game. - Merge pull request #343 from clifftaylor2325/work/brand-crest
media: Vanguard's crest as the addons-list icon - media: Vanguard's crest as the addons-list icon
Media/Textures/Icon.tga is now the crest: two swords crossed behind a
rimmed heater shield that bears a serif V, drawn at 64 px by the new
scripts/lib/media-brand.mjs with media-art's shapes (no new dependency)
from the reference drawing art/brand/vanguard-crest.svg, the same drawing
the website uses for its logo and favicon. The two-diamond coloredIcon is
gone; LogoLeft/LogoRight (the in-game T.Logo) are unchanged.
Also: iconIsCrest in scripts/media.test.mjs (brass, slate and steel pixel
counts), the crest at the top of README.md, a note in docs/THEME.md
("Icons") and art/CREDITS.md (the V is Cinzel's capital, OFL, as an
outline), and docs/handoff/BRAND-CREST.md for the session that finishes
this on the owner's machine.
Verified: node --check on the changed .mjs; Icon.tga written by the same
tga(crestIcon(64)) call the build makes; the icon's outer pixel ring is
clear; the new test's counts run by hand (714 brass, 102 slate, 40 steel).
Not run (npm registry blocked in the sandbox): npm run verify, coverage,
build-media --check, markdown lint. - Merge pull request #342 from clifftaylor2325/work/spellbook-title-band
Windows: the spellbook's title and controls inside its slate band - Windows: the spellbook's title and controls inside its slate band
From /vg frame: the title container spans 14 to 34 below the frame's top
and the controls 12 to 36, while the parchment container starts 14 down,
so both ran across the line. TITLE_RAISE lifts the title container 10 and
CONTROL_DROP puts the controls level with it.
Verified: windows-map test, npm run quality, npm run coverage (100%).
Needs in-game check. - Merge pull request #339 from clifftaylor2325/work/map-window-controls
Windows: the map's close and maximize buttons level with its title - Merge pull request #341 from clifftaylor2325/work/split-windows-tools
Options: split Windows and tools into Game windows and Combat tools - Merge remote-tracking branch 'origin/main' into work/split-windows-tools
- Merge pull request #340 from clifftaylor2325/work/switch-polish
Options switch: wrap captions, nicer track and knob - Merge pull request #338 from clifftaylor2325/work/options-esc-close
Options window: Escape closes it - Merge main into work/split-windows-tools
- Merge remote-tracking branch 'origin/main' into work/switch-polish
- Options switch: wrap long captions, recessed track, beveled knob, hover and press
A switch row's caption now wraps to the width left of the track on every page and its row grows to fit. The track is 36x18 with an inner border and accent fill, the knob has a light top and dark bottom line, brightens under the mouse and the fill darkens while pressed. Verified: quality, coverage. Needs in-game check. - Options: split Windows and tools into Game windows and Combat tools
Game windows keeps the game windows' switches, auto-accept and tooltips.
Combat tools takes the Cooldown Manager and damage meter rows, Vanguard's
cooldown row and glow, boss mods, fight history, the Vanguard meter and the
roll tracker (and Officer loot's card, which adds to the tools page). The
tools key stays, so rows other code adds to it still land on a page. The
options window is 600 px tall, not 560, so the 19 nav entries stay about 26 px. - Windows: the map's close and maximize buttons level with its title
The map's title sits 14 down, the other windows' about 24, so its window
controls hung below it and crowded the quest log's search row. PlaceClose
takes an optional drop and GW.CONTROL_DROP sets it for the map.
Verified: windows-map test, npm run quality, npm run coverage (100%).
Needs in-game check. - Merge remote-tracking branch 'origin/main' into work/options-esc-close
- Options window: Escape closes it
The window holds the keyboard while open, so Escape never reached the game's close-on-Escape list. onKey now hides the window on Escape unless a box has the typing (the box clears first).
Verified: options test, quality, coverage 100%. - Meter: bigger panel, clear of the tooltip, figures to two decimals (#337)
The panel is 300 wide with 18-unit rows, shows eight lines by default (three to ten)
and starts left of the bags and menu bar so the tooltip corner no longer covers it.
A plain figure is rounded to two decimals here (trailing zeros dropped, K and M
kept); a hidden one still goes to AbbreviateNumbers untouched.
Verified: meter tests (decimals, size, secret figure), quality, coverage 100%. - Merge pull request #335 from clifftaylor2325/claude/raid-frame-default-left-yoxuvx
Raid frames: pin the grid to its spot's side right after a reset or drag - Merge pull request #336 from clifftaylor2325/work/nav-icon-alignment
Options nav: line up the Unit frames icon with the others - Options nav: a squarer icon for Unit frames, Town and Tools
The framed portrait is only 18 of 32 pixels wide, so Unit frames' icon sat
off the column the other icons make and left a gap before its label. Unit
frames takes the orc head, Town and gear the castle and Windows and tools
the battle axe, all from art/game-icons, each 26 px wide like most entries. - Raid frames: pin the grid to its spot's side right after a reset, drag or nudge
The grid was pinned to the holder's side only at the next roster change, so a reset (or a drag to the other side) left it anchored to the old side until then. Core/Movers.lua gets ns.OnMoverPlaced, told by ns.MoverChanged (which a reset now calls too); the raid frames refit on it. README says how to forget a saved spot (/vg reset raid).
Verified: setuplayouts reset case, movers and unlock tests, quality, coverage 100%. Needs an in-game check. - Vanguard meter and cooldown spell row (both off by default) (#329)
- meter: Vanguard's own damage and healing meter (off by default)
A new Meter module draws a small movable flat panel with the top lines of
the game's damage meter (C_DamageMeter) for the current fight: up to eight
theme bars in class colors with a rank, name and per-second figure, yours
in the accent. Click the title or /vg meter dps|hps to switch damage and
healing. Read every half second, in combat too: hidden figures go straight
into the bars and texts and are never counted, compared or sorted, a widget
that refuses one leaves the row as it was, and a hidden list shows
"Nothing to show". The panel is built only when switched on, so an unused
meter has no mover. Default spot is the game's meter's, which it replaces.
Verified: scripts/tests/meter.test.mjs (8 cases), options row counts,
npm run quality, npm run coverage (100%, 23490/23490). luacheck is not
installed here. - meter: review fixes (hidden list entries, command with the module off, no read while hidden)
A hidden or non-table entry in the list is skipped, including as the top
line the bars scale to. /vg meter says so when the module is switched off
instead of failing. A panel that is not shown no longer reads the game's
meter. AbbreviateNumbers' answer is returned without a truthiness test.
Verified: meter.test.mjs (10 cases), npm run quality. - cooldowns: a Vanguard-drawn spell row in the gap row, from your picks or the manager's list
The Cooldown gaps row now also draws spells: ones you choose with
/vg gaps spell add <spell|id|link>, and, behind a new switch (off by
default), the spells the game's Cooldown Manager tracks in its essential
and utility rows, read through C_CooldownViewer when they change. Each
icon sweeps from the client's duration object, else plain numbers; a spell
with no object and hidden numbers is drawn as ready. Charges, tracked buffs
and buff bars stay with the game's manager, which is left as it is.
Verified: cooldowngaps.test.mjs (5 new cases), options row counts,
npm run quality, npm run coverage (100%, 24245/24245). - cooldowns: review fixes for the spell row (no guessed ready, global cooldown left out, cap 20)
A spell whose timer and numbers are both unreadable now leaves its icon
out instead of showing it as ready. The spell sweep asks for the duration
object without the global cooldown, and a plain global cooldown clears the
sweep. The row holds up to 20 icons. Spec changes of other players no
longer re-read the manager's list, and the end of a fight does.
Verified: cooldowngaps.test.mjs (16 cases), npm run quality, npm run
coverage (100%, 24249/24249). - Options row counts after the options split
- meter: Vanguard's own damage and healing meter (off by default)
- Merge pull request #311 from clifftaylor2325/work/group-threat
215 GroupThreat: party threat panel that hides when threat is secret - Merge origin/main into work/group-threat
- Merge pull request #331 from clifftaylor2325/claude/raid-frame-default-left-yoxuvx
Raid frames: 25 and 40 start left of your character, groups stacked as rows - Merge origin/main into claude/raid-frame-default-left-yoxuvx
- Merge pull request #334 from clifftaylor2325/claude/memory-growth-iirefv
perf: stop making strings for every chat line, add /vg mem - Merge pull request #330 from clifftaylor2325/work/split-bars-map-chat
Options: split Bars, map, chat into five pages - Merge remote-tracking branch 'origin/main' into claude/raid-frame-default-left-yoxuvx
- Raid frames: fix luacheck shadowing, and refit the grid after combat when the preview ends at combat start
The preview's Stop ran RF.Fit from the PLAYER_REGEN_DISABLED handler, where ns.OutOfCombat can let the secure header's attribute writes through before the lockdown; it now uses ns.AfterCombat there. Test covers the edge. - Merge main into work/split-bars-map-chat
- Merge remote-tracking branch 'origin/main' into claude/memory-growth-iirefv
- Slider values: round the text and keep it in its slot (#333)
- slider: show values rounded and keep the label in its slot
The default slider text was tostring, so width and height showed
210.00007629395. It now rounds to the step's decimals, and the value label
has a fixed right-aligned width. Verified: new options test, quality, coverage. - slider: split value text into a helper, comment test helper
- slider: show values rounded and keep the label in its slot
- Raid frames: test that a roster change in combat re-anchors nothing until combat ends
- Merge remote-tracking branch 'origin/main' into claude/memory-growth-iirefv
- perf: stop making strings for every chat line, add /vg mem
Group board, Chat watch and Chat extras each made new strings for every chat
line in a busy city: Group board built " phrase " 18+ times per line and ran
four gsubs, Chat watch ran five string passes even with no phrases watched, and
Chat extras formatted the accent color and made two closures per line (once per
chat window) before looking for an address. Now:- Group board pads each phrase once and skips the code strips on a line with no
escape codes. - Chat watch returns before reading a line when no phrase is watched, and skips
the code strips on a plain line. - Chat extras looks for "://" or "www." before it builds anything.
/vg mem watches Vanguard's memory for a minute (a reading every 5 s) and says how far
it moved, the low and high, and whether the low point rose (memory kept) or
held (garbage the game collects). Readings are skipped in combat, and
/vg perf now leaves the game's memory counter alone in combat too.
Verified: npm run quality, npm run coverage (100%). Needs an in-game check: the
real memory numbers (/vg mem in a city, smoke test 1.5).
- Group board pads each phrase once and skips the code strips on a line with no
- Merge remote-tracking branch 'origin/main' into claude/raid-frame-default-left-yoxuvx
- Merge pull request #332 from clifftaylor2325/work/sized-icons
232 media: menu strip icons at their on-screen sizes - Merge main into work/sized-icons
- Raid frames: up to 10 stay over your frame, 25 and 40 move left and stack as rows
Cliff: a party or a raid of 10 is fine directly over the player frame; for 25 and 40 the groups should go left of the character and stack vertically. The raid frames' default spot and groups' direction now follow the group size: up to 10 as before (centered over the player frame, columns); more than 10 at the left (left edge 620 left of the middle, 344 up) with the groups stacked as rows. A new Stack big raids at the left option (on by default) turns it off. Party frames and party extras keep their old spot. Setup's Healing layout no longer saves a spot of its own; it puts the defaults back and turns click casting on. A saved spot wins at every size.
Verified: setuplayouts overlap checks at 1365x768, 1920x1080 and every screen preset, new size-based spot test, raid preview and options tests. Needs an in-game check. - 232 media: menu strip icons baked at their on-screen sizes
Draws the 16 menu strip icons by signed distance at 16, 20, 24 and 32 px
(drawSdf: one-pixel smoothstep, level and upright stems snapped to whole
pixels, a clear pixel at each edge). T.Icon(name, size) picks the baked size
when the new sizedIcons setting is on (/vg icons, off by default, after a
/reload); the menu strip passes MS.GLYPH_PX. Defaults picked: only the icons
the menu strip uses, no 48 px size, no Options row (slash command only),
glyph size assumed to be about 20.
Verified: npm run quality, npm run coverage (100%), media --check, media tests
(Lua list matches the generator, every file is its stated size). - Merge main into work/split-bars-map-chat
- Merge remote-tracking branch 'origin/main' into claude/raid-frame-default-left-yoxuvx
- Merge pull request #317 from clifftaylor2325/work/cdm-gaps
221 cooldowns: a display-only gap row for items, pet abilities and totems - Merge main into work/cdm-gaps
- Merge pull request #318 from clifftaylor2325/work/cdm-glow
252 cooldowns: refresh-window glow on the Cooldown Manager - Merge main into work/cdm-gaps
- Merge origin/main into work/group-threat
- Merge main into work/cdm-glow
- Merge pull request #324 from clifftaylor2325/work/officer-loot-tools
222 loot: officer soft-reserve list and group roster (off by default) - Merge remote-tracking branch 'origin/main' into claude/raid-frame-default-left-yoxuvx
- Raid, party and party extras default to the left of your character
Healers keep these frames to the left of their character so the boss and their own frames stay in view. The raid grid, the party column and the party pets and targets now start with their left edge 620 units left of the screen's middle (344, 470 and 606 up), growing right and up; a full 40-man ends at the middle and still fits a 1365 wide screen. Setup's Healing layout saves the same spot. Frames with a saved spot keep it.
Verified: setuplayouts overlap checks (1365x768, 1920x1080, every screen preset), new default-spot test, setup tests, quick check. Needs an in-game check. - Merge main into work/officer-loot-tools
- Merge main into work/split-bars-map-chat
- Merge main into work/cdm-gaps
- Merge origin/main into work/group-threat
- Merge main into work/cdm-glow
- Merge pull request #327 from clifftaylor2325/work/polish-toggles
Options switch: larger track and knob - Merge pull request #328 from clifftaylor2325/work/classic-look
218 theme: Classic look variant - Merge main into work/classic-look
- Options switch: larger track and knob
The on/off switch is 36x18 with a 14px square knob, easier to read and hit. Verified: quality, coverage. Needs in-game check. - Merge pull request #326 from clifftaylor2325/work/modules-toggle-overlap
Options modules: wrap descriptions left of the toggle - Split the Bars, map, chat options page into five pages
Bars and bags, Map, Chat, Windows and tools, and Town and gear, each with its
own nav icon (from the game-icons.net silhouettes already in art/). Every
setting keeps its key and behaviour; only the page a row sits on changes.
The nav entries shrink to fit when the pages would run past the nav's foot,
Nameplates takes order 65 so the nav order no longer depends on a sort tie,
and Vanguard.OpenOptions("more") still opens (Bars and bags). - 218 theme: Classic look variant (bronze and gold-cream, ornaments on)
A fourth theme variant in T.VARIANTS: dark leather panels, a bronze border, gold-cream text and the window ornaments always on, all our own colours and the existing ornament art; nothing from Blizzard. The panel border now reads T.RGB(edge) so a variant can colour it. Also adds the missing What's new line for the variants. Verified: quality, coverage. - Merge remote-tracking branch 'origin/main' into work/modules-toggle-overlap
- Options modules: wrap descriptions left of the toggle
Module row descriptions now wrap to the width left of the switch and the row is taller for two lines. Verified: test:quick, quality, coverage. Needs in-game check. - Merge main into work/cdm-glow
- Merge pull request #325 from clifftaylor2325/work/ornament-kit
238 theme: window ornament kit (brass rule, corner brackets) - Merge fix: group threat notes after main; one Instance prep group in What's new
- Merge main into work/ornament-kit
- Merge pull request #308 from clifftaylor2325/work/p8-spellbook-buttons
P8 windows: calendar Filters clear of close; pane arrow and finder gear on the resting tile - Merge origin/main into work/group-threat
- Merge main into work/cdm-glow
- Merge pull request #304 from clifftaylor2325/work/raid-readiness
214 InstancePrep: buff checks on the ready-check card - Merge remote-tracking branch 'origin/main' into work/p8-spellbook-buttons
- Merge main into work/ornament-kit
- Merge origin/main into work/raid-readiness
- Merge pull request #322 from clifftaylor2325/claude/project-thread-hpqyz5
Test speed: faster harness and area-sized test files (#147, #148) - Merge remote-tracking branch 'origin/main' into claude/project-thread-hpqyz5
- 214: options row count after merging main
- Merge origin/main into work/raid-readiness
- Merge main into work/ornament-kit
- Merge pull request #321 from clifftaylor2325/work/theme-variants
237 theme: High contrast and Vellum and ink variants - Merge main into claude/project-thread-hpqyz5
Main again edited the cases of auction, setup, more, core, profiles and plates and added
SetIsFrameBuffer to the stub; the area files are regenerated from main's versions (the
auction tabs rename, two boat timer cases, later edits), no case added or dropped. - Merge main into work/theme-variants
- Merge pull request #323 from clifftaylor2325/work/legacy-planner
216 legacy: probe of the Legacy system and a 16-point perk plan list - Merge pull request #312 from clifftaylor2325/claude/project-thread-tgcytk
Auction: a bag click keeps Posting open; tabs renamed Lists and Posting - CooldownGlow: fold the unusable-answer branch so every line runs
- P5 AuctionHelper: review fixes for the Posting bag click
- The display-mode hook records every mode set, not only changes, so
leaving Posting for the game's Sell tab and then clicking a bag item
no longer pulls the window back to Posting. - KeepSelling runs only when the Sell card took the item (TakeItem now
returns true), so an item the card refuses stays on the game's page. - Closing the auction house forgets the modes.
- README names "the Lists tab" and "the Posting tab" so the tab and the
Lists pane read apart; preview captions and test comments use the new
names.
Verified: new case in auctionpost.test.mjs (Posting, then the game's
Sell, then a bag click stays on the game's sell page); npm run quality
OK; npm run coverage 100% lines; luacheck 0 warnings.
- The display-mode hook records every mode set, not only changes, so
- Merge main into work/theme-variants
- Merge main into work/ornament-kit
- 252 cooldowns: GetModule lookup inside a multi-line function (module registry test)
- 238 theme: comment the ornament test helper
- Merge origin/main into work/cdm-glow
- Merge main into work/legacy-planner
- P8 windows: calendar Filters clear of close; pane arrow and finder gear on the resting tile
Follow-up to #314, which put every window control on a faint raised
tile. Two control rows still looked off next to it:- Calendar: the game anchors the Filters dropdown 27 units in from the
top-right corner, which ran it into the close button's tile once
GW.PlaceControls put the close button 24 in. New GW.BesideClose puts a
wide title-row button level with the close button, its right edge
where a maximize box's tile would end (T.CONTROL.step left of the
close button's middle, less half a tile, pixel-rounded). - The character pane's right-half arrow and the group finder's gear sit
in the control row but keep their own icons, so they had no tile.
New T.ControlPlate draws only the resting tile (T.CONTROL_TILES.rest,
the tile's size, centered, under the icon) behind such a button.
Docs: README (calendar Filters, pane arrow, gear), docs/THEME.md
(T.ControlPlate), new smoke step 5.3a.
Verified: windows-map checks the Filters anchor and the gear's tile;
windows-tabs checks the arrow's tile (raised at 0.9, tile size, made
once). npm run quality passed; npm run coverage passed every test at
100% Lua line coverage (23026/23026). Needs an in-game check (5.3a).
- Calendar: the game anchors the Filters dropdown 27 units in from the
- 252 cooldowns: luacheck, split the curve guard so 'duration' is always set
- Merge pull request #315 from clifftaylor2325/work/fight-recap
217 fights: post-fight recap from the game's damage meter - Merge origin/main into work/cdm-glow
- Merge main into work/fight-recap
- Merge origin/main into work/officer-loot-tools
- Merge origin/main into work/raid-readiness
- Merge pull request #307 from clifftaylor2325/work/boat-timers
220 errands: boat and zeppelin timer - Merge main into work/boat-timers
- Merge origin/main into work/cdm-gaps
- Merge remote-tracking branch 'origin/main' into claude/project-thread-tgcytk
- Merge origin/main into work/cdm-glow
- 222 loot: officer soft-reserve list and group roster, off by default
Adds Modules/OfficerLoot.lua and OfficerRoster.lua (listed in Vanguard.toc), one
module behind one setting, off until switched on (/vg loot on, or the card under
Bars, map, chat). A soft-reserve list is kept per character in VanguardCharDB
(/vg loot add|remove|list|clear|focus), and the roll tracker (Modules/Rolls.lua,
extended through a small hook, not duplicated) flags a roll on a reserved item
"[SR]" and lists who reserved a loot-history item. A small movable roster panel
lists the group by role with class colors, Lead/Asst, Offline and Dead. Sharing
is by hand: buttons put the list in the chat box behind /p or /raid (or print it
to your own chat); no addon messages, nothing sent, rolled or assigned for anyone.
Reads of the group go through ns.IsSecret. Smallest reasonable choices: reserves
are keyed by item id (from a link) else lower-case name; the round's item is the
newest loot-history item still rolling, or /vg loot focus.
README, /vg help, the 0.6.0 What's new notes and docs/SMOKE-TEST.md (section 20) updated;
.luacheckrc gains UnitIsGroupLeader and UnitIsGroupAssistant. No fade or sound added.
Verified: scripts/tests/officerloot.test.mjs (8 cases) passes; options page row counts
updated; npm run quality and npm run coverage pass (100.00%); the full suite passes
(one unrelated nameplate case timed out under machine load and passed alone).
Not observed in game: ChatFrameUtil.OpenChat, links in slash commands, other members'
death events, loot history. - Merge pull request #301 from clifftaylor2325/work/theme-experiments
239 theme: SLUG world text and frame-buffer window fades (off by default) - Merge main into work/ornament-kit; keep build functions within ten lines
- Merge main into claude/project-thread-hpqyz5
Main changed the info strip cases in more.test.mjs again (row counts, durability text, the
separator); the area files are regenerated from main's version, no case added or dropped. - Merge main into work/theme-experiments
- 212 dungeons: a dungeon card on the Instance prep checklist, off by default (#316)
Dungeon card setting on Instance prep: the dungeon's level range, your own quests for it and the keys you carry, from the client and your quest log only. - Merge pull request #320 from clifftaylor2325/claude/project-thread-umx362
#219 mail tools: inbox search and filter, send-to-alt presets (off by default) - Merge main into work/fight-recap
- Options test: row count after merging main
- Merge main into work/legacy-planner
- Merge remote-tracking branch 'origin/main' into claude/project-thread-umx362
Conflicts:
docs/SMOKE-TEST.md
scripts/tests/options.test.mjs
- 238 theme: window ornament kit (brass rule, corner brackets), off by default
New generated Media/Textures/OrnamentCorner (a one-pixel L with a 45 degree chamfer) and T.Ornament(frame, kind, anchor): a rule with the Diamond cut on the options window, brackets on setup and the quest log, tinted in the accent's dim variant. /vg ornaments and General > Look > Window ornaments switch it. The faction crest watermark is not built. Verified: quality, coverage. - Merge pull request #314 from clifftaylor2325/claude/project-thread-plseac
Window controls on a resting tile; info strip in two columns with a bigger calendar button - 216 legacy: probe of the Legacy system's data and a 16-point perk plan list
/vg legacy probe lists what the client has for the Legacy system (window pages, perk panel ids and node count, Legacy globals), read only and out of combat, so a challenge tracker can be built on real data. A perk planner, off until switched on, keeps a hand-typed list of perks within the tree's 16 points for the account. No challenge or perk names are built in: no data source has been seen.
Verified: npm run coverage (100%), code-quality scan. Needs an in-game check (SMOKE-TEST 15.6). - Merge main into work/boat-timers
- 211 spells: rank-aware bar notice and trainer memory, off by default (#305)
New SpellRanks module: a bar notice with /vg ranks and /vg ranks swap, and trainer memory with /vg trainers. Both switches start off. - 239 theme: guard the frame-buffer fade handler
- Info strip: two columns, taller rows, labelled durability
The strip lays its blocks in two columns (left and right) instead of three, so no two run together: durability beside free bag slots, gold beside the clock, fps and latency beside ammo or shards, the date with its calendar button, then the leveling pairs and the DPS pair. Rows are 18 units. Durability reads '100% durability' and system '27 fps ยท 44 ms'. README and smoke test 14.5 updated; height assertions follow. - Merge main into claude/project-thread-hpqyz5
The split test files are regenerated from main's versions of auction, setup and more, so the
cases main changed since the split (the Post confirm flow, the info strip date, the menu
button badge, the quest log reward tooltip) are carried over unchanged. - Merge pull request #313 from clifftaylor2325/claude/draggable-panels-8sdaie
#201 windows: drag by the top edge, and the calendar and 14 more windows - 148 tests: split more, auction, core, profiles, setup, plates and preview into area files
more.test.mjs becomes chat, quests, strips, alerts and extras; auction becomes auction,
auctionshop, auctionsell and auctionowned; core becomes core, clientprobe and errors;
profiles gains profileguard, setup gains setuplayouts, plates gains platepaint and
platebuffs, preview gains previewodd (scenes by position). Every case moved as it was;
helpers a file needs are copied into it. login-scope.json and FULL_CASES follow the cases.
Verified: 1293 cases before and after; npm run coverage passes at 100% (22140/22140);
the slowest file went from 207 s to 67 s on a 4-core box. - 147 harness: widget methods from per-class tables, cached listener order, scroll owners and inspect chunk; the Lua project keeps its workers
What changed: a widget indexes a per-class table of resolved methods (kind, with or
without the backdrop mixin) instead of a Lua __index function, and a missed name is
remembered; stub.metaFor picks the metatable again when the backdrop mixin or the owner
changes. fire and update sort a set of listeners only when its members changed.
scrollOwner reads a map filled by SetScrollChild instead of scanning every frame.
wow-inspect.lua goes through the chunk cache. The lua Vitest project runs with
isolate: false so a worker keeps its chunks and warm JIT from file to file.
Verified: a warm create + login + unlock went from 2236 ms to about 1230 ms on a 4-core
box; the full suite (1293 cases) passes unchanged; npm run quality is clean. - Merge main into work/theme-experiments
- Merge remote-tracking branch 'origin/main' into claude/project-thread-umx362
Conflicts:
docs/SMOKE-TEST.md
scripts/tests/options.test.mjs
- #219 mail tools: inbox search and filter, send-to-alt presets, off until switched on
Adds Modules/MailTools.lua. A panel beside the mail window searches the inbox (every word in sender, subject or first item) and filters it (All, Money, Items, Sales); Take takes a mail's money and its first item, one click each. /vg mail prints the same in chat. Presets (/vg mail preset add|remove, list) fill the Send Mail tab for an alt: a preset's button or /vg mail send types the character in and attaches matching bag items, up to 12, out of combat; the player presses Send. Options switch, README, /vg help, smoke test 21, What's new (off). Harness gets a test-only mail window and Take stubs.
Verified: npm run quality and npm run coverage (100%). Needs in-game check: see the PR. - 252 cooldowns: keep gameToolRows short; update options row counts
- Merge pull request #309 from clifftaylor2325/claude/social-window-fixes-pjumas
Social windows: guild panes' fill goes under the rows - Quest log: reward cards show the item tooltip on hover (#310)
The quest log's reward cards had no hover script, so hovering the
Sustaining Ring (or any reward) showed nothing. The card now wires its own
tooltip (QP.WireTooltip): a log reward is set with SetQuestLogItem from its
slot and quest, a dialog reward with SetQuestItem as before. The dialog's
duplicate OnEnter/OnLeave and QD.ItemTooltip are gone.
Verified: new quest log hover test (reward, choice, leave, client without
the call), harness GameTooltip:SetQuestLogItem, npm run quality, npm run
coverage (100%). Not verified in game. - 217 fights: post-fight recap from the game's damage meter
/vg recap shows the last fight's interrupts, dispels, damage taken and deaths from the game's damage meter; /vg recap say|party|raid|instance|guild sends that one line on your command, out of combat. A chat recap after each fight is a setting, off by default. Parts the meter lacks or hides are left out. The info strip's meter read now takes the meter kind.
Verified: npm run coverage (100%), code-quality scan. Needs an in-game check of the meter kinds and fields (SMOKE-TEST 7.5). - 221 cooldowns: a display-only gap row for items, pet abilities and totems
Adds Modules/CooldownGaps.lua (listed in Vanguard.toc), off by default behind
one setting (Bars, map, chat > Cooldown gaps). A small movable row (ns.Mover
cdm_gaps) of icons dressed like the Cooldown Manager reskin: on-use trinkets
you wear and items chosen with /vg gaps add (C_Item / C_Container), pet bar
abilities (C_Spell duration object, else GetPetActionCooldown) and totems
(GetTotemDuration, else GetTotemInfo; skipped while the class bars show them).
Duration objects go straight to the sweep; plain numbers are drawn only when
ns.PlainNumber reads them; a hidden cooldown hides the icon. Each source
degrades to nothing where the client lacks the API. No clicks, nothing
protected, no fades or sounds.
Also: /vg help line, README paragraph, 0.6.0 What's new line, SMOKE-TEST 20.1,
THEME.md note, read_globals for the three new game globals, options row counts.
Verified: new cooldowngaps.test.mjs (11 cases, incl. secret item, pet and
totem values and a secret-length duration object) passes with 100% line
coverage of the module; npm run quality OK; npm test passes (the 15 cases
that hit the 30 s timeout on this slow runner pass on rerun with a longer
timeout). Full npm run coverage was killed on this runner (exit 143) and not
completed. In-game behaviour not observed. - Merge remote-tracking branch 'origin/main' into work/close-and-calendar
- Merge remote-tracking branch 'origin/main' into claude/draggable-panels-8sdaie
- #201 windows: drag by an empty spot along the top edge, and the calendar and 14 more windows
The title-bar drag area (#201) only covers windows with a title bar part, and
the calendar and others were not listed. Every movable window now also takes a
press on an empty spot along its top 30 units (a hook on the window's own
OnMouseDown/OnMouseUp, so it never takes a click from a button), and the
calendar, inspect, trade, dress-up, trainer, guild bank, macro, stable, raid,
letters, loot history, books, add-ons list, help and quest popup windows are
dragged that way only (no drag area over them). A click that never moves the
window saves no place; hiding mid-drag lets go.
Verified: windows-move tests (9), npm run quality, npm run coverage (100%).
Needs in game: the drag itself, whether each window has mouse enabled. - Merge pull request #306 from clifftaylor2325/work/integration-api
251 api: options deep links, preset registration and built-in role presets - P5 AuctionHelper: a bag click keeps Posting open; tabs renamed Lists and Posting
Cliff's video: with Vanguard's Selling tab open, clicking an item in the
bags switched the auction window to the game's own Sell page. The game's
SetPostItem sets its own sell display mode; the card took the item but
was hidden.- The window's display-mode hook keeps the mode before the latest one
(AH.lastMode). The SetPostItem post-hook, after the card takes the
item, shows our page again when it was the one open (AH.KeepSelling).
From the game's own tabs, a bag click still shows the game's Sell page. - Cliff asked to rename the tabs because Shopping and Selling match
Auctionator. They are now Lists and Posting (AH.TAB_NAMES; the default
on the open decision card), in the tab row, the title, the chat lines,
the options captions, What's new, README and SMOKE-TEST 9.4 / 14.9.2.
Internal names (frames, functions, the default list "Shopping") stay. - What's new 0.6.0 gains lines for the rename, the posting guide and
Confirm, and the bag click.
Tests: a new auctionpost case reproduces the switch (fails without the
fix) and checks the guide fits the card; bag clicks in the Post tests no
longer click back to the tab. npm run coverage: 1302 tests, 100% lines;
quality OK; luacheck 0 warnings in 137 files.
- The window's display-mode hook keeps the mode before the latest one
- 215 GroupThreat: party and raid threat panel on your target, off by default, hides when threat is secret
Adds Modules/GroupThreat.lua: with the new 'show' setting on (Unit frames, reload) a small movable
panel lists up to five group members by threat on your target. It hides itself when
C_Secrets.ShouldUnitThreatValuesBeSecret says so (or errors / answers secret), or when any value
or name comes back secret; a hidden value is never compared. Plain frame, so it updates in combat.
Decided as option (a) on the issue. Whether Forever hides threat is for the #223 in-game test.
Verified: npm run verify, npm run coverage (100%). - 251 docs: API.md presets heading is a section, not a second title
- theme test: a hidden control's tile goes back to the resting slate
- Merge main into work/integration-api
- Merge remote-tracking branch 'origin/main' into claude/social-window-fixes-pjumas
- Social windows: guild panes' fill goes under the rows, not over them
The guild window's InsetFrames sit at frame levels 100 and 200, above the roster,
community list and chat rows, so Vanguard's inset fill (95% opaque) drew over
them and dimmed every name, zone, rank and note. The fill now lives on a frame
one level below the pane; the inset keeps only its border.
Verified: windows-social test (fails without the change), npm run quality,
npm run coverage (100%). Needs an in-game check. - Merge pull request #303 from clifftaylor2325/claude/project-thread-umx362
#213 craft queue: queue, profession cooldowns and recipe ranking (off by default) - Merge main into work/integration-api
- 220 errands: boat and zeppelin timer, learned from your own rides
A ride is spotted when the game gives no map position outside an instance, a flight and death, three checks running. Landing saves the time by route (sub-zone where you boarded > where you got off); the next ride from that dock counts down from the route taken last. Off until switched on (Errands.boatTimer), polled once a second, never in combat.
Verified: npm run verify, npm run coverage (100%). Needs an in-game check that boats give no map position (SMOKE-TEST 9.6). - 251 api: annotate the preset helpers
- Window controls: a faint tile at rest; info strip calendar button bigger
Close, maximize and minimize now sit on a faint raised-slate tile at rest (it turns red or accent under the mouse), so the cross is anchored on every panel. The info strip's calendar button is 18 units with a 14-unit icon (Minimap.SlotButton takes an optional glyph size). THEME.md, smoke test 5.1 and tests updated. - Merge remote-tracking branch 'origin/main' into claude/project-thread-umx362
Conflicts:
scripts/tests/options.test.mjs
- 237 theme: High contrast and Vellum and ink variants, off by default
A variant setting (General > Look > Theme variant, /vg variant) swaps surface and text tokens in Core/Theme.lua: T.VARIANTS, read first by T.RGB and T.PanelColor. High contrast also turns Color assist on, drops the sheen and gradients and draws accent borders 2 px thick. It is saved in the profile and its export. Parchment waits (decided b). Verified: verify, coverage. - Merge pull request #298 from clifftaylor2325/claude/project-thread-tgcytk
Auction Post: a posting guide on the Sell card, Confirm when the game asks, and chat reports - Merge pull request #302 from clifftaylor2325/claude/project-thread-plseac
Info strip: day, date and a calendar button - Merge remote-tracking branch 'origin/main' into work/integration-api
- 251 api: options deep links, preset registration and built-in role presets
Adds Vanguard.OpenOptions, Vanguard.RegisterPreset and Vanguard.GetPresets (Core/Integration.lua),
Healer, Damage and Tank partial profile strings (Core/PresetStrings.lua), a Presets block on the
Profiles page and /vg preset. A preset applies only after the player says yes, out of combat, through
the existing import path (backup first). Documented in docs/API.md and README; what's new and smoke
test updated. Verified: npm run verify, npm run coverage (100%). - P5 AuctionSell: a posting guide on the Sell card, and Confirm when the game asks
Cliff asked for posting that tells players exactly what is happening and
when, and picked the step guide on the card.- Under the Post line: four steps (1 Item, 2 Price, 3 Post, 4 Listed) with
the current one lit, and a line saying what to do next or what is
happening (checking the price, click Post for stack 1 of 10, stack 1
sent and waiting, finished). Why Post is off moves from the count line
to the guide, in the warn colour; the count line is just the count. - The game's last answer to a post (from the chat report) shows in the
guide until the next click. - A post the game wants confirmed is no longer left to the game's own
Sell tab: Post reads Confirm and the next click calls ConfirmPostItem /
ConfirmPostCommodity with the same arguments (one call per click). - README, SMOKE-TEST 14.9.2 and the module headers say so.
Verified: npm run coverage (1281 tests, 100% lines), quality OK,
luacheck 0 warnings in 137 files. Needs an in-game check: the guide's
look and wording, and Confirm (the game's own confirm window may also
appear and can't confirm Vanguard's post).
- Under the Post line: four steps (1 Item, 2 Price, 3 Post, 4 Listed) with
- 214 InstancePrep: buff checks on the ready-check card (flask, food, weapon oil, raid buffs), off by default
Adds Modules/InstancePrepBuffs.lua: with the new 'buffs' setting on, the Instance prep card gets
'missing' rows for a flask, the Well Fed food buff, a timed weapon enchant on the main hand (and
off hand when it holds a weapon) and the raid buffs Fortitude, Mark of the Wild and Arcane
Intellect; 'worldBuffs' adds a row when you carry none. Out of combat only; no row while aura
values are hidden (ns.AurasSecret). Buff names are from classic and unverified on Forever.
Verified: npm run verify, npm run coverage (100%). - Merge remote-tracking branch 'origin/main' into claude/project-thread-umx362
- #213 craft queue: queue, profession cooldowns and recipe ranking, off until switched on
Adds Modules/CraftQueue.lua. /vg craft add queues the recipe on display (kept per character), lists what reagents are short, next opens the first recipe with its count typed in, stock raises Keep stocked and makes the Restock list, a craft you finish comes off the queue. It also notes profession cooldowns across characters (/vg craft cd) and ranks recipes with every price known (/vg craft top, per craft and per hour at a set pace). Reuses AuctionCrafting's recipe pricing. Nothing is bought, posted or crafted for you; profession calls are feature-detected. Options row, README, /vg help, smoke test 20, What's new (off).
Verified: npm run verify and npm run coverage (100%). Needs in-game check: see the PR. - Merge pull request #300 from clifftaylor2325/work/p8-bag-sort-fix
Bag window: Sort no longer under the backpack icon; items drag again (quest tracker under windows) - Merge pull request #299 from clifftaylor2325/work/menu-badge-clicks
#243 menu: badged micro menu buttons take clicks - Info strip: guard the calendar button's tooltip handlers
- Merge remote-tracking branch 'origin/main' into work/minimap-calendar
Conflicts:
docs/SMOKE-TEST.md
- Info strip: day, date and a calendar button
A third strip row shows the day and date with a flat calendar button at its right end that opens the calendar (ToggleCalendar). The leveling and DPS rows move down one. Own-drawn Calendar icon; Minimap.SlotButton takes an optional size. New Day, date and calendar button switch under Bars, map, chat; the Situations key list includes it. README, smoke test 14.5 and tests updated.
Verified: npm run verify and npm run coverage (100%). - 239 theme: SLUG world text and frame-buffer window fades, both off by default
/vg slug adds the SLUG font flag to outlined world-text roles (T.FontFlags); /vg framebuffer draws a window through SetIsFrameBuffer only while it opens and closes, where the client has the call. SetIsFrameBuffer added to the widget allowlist (documented since 10.0). Radial progress and SVG textures are not wired: their signatures are unconfirmed on Forever. Verified: verify, coverage 100%. - P8 quest tracker: on LOW strata, under the game's windows
Cliff couldn't click and drag items out of the combined bag. His
screenshot shows Vanguard's quest tracker drawn over the bag window: the
tracker's frame was on the default MEDIUM strata, the same as the bag,
and above it, so its clickable quest titles (Buttons) took the mouse from
the bag slots under them.
The tracker frame (its titles with it) and its secure quest item buttons
now use LOW, the strata of the game's own ObjectiveTrackerFrame, so game
windows draw over them. The item buttons get it when they are made, which
is out of combat (QT.PlaceItems runs through ns.OutOfCombat).
Verified (harness only): more.test.mjs quest tracker cases. README
sentence and smoke step 9.6 added. Needs an in-game check. - #187 Bags: hide the gamepad backpack icon that covered Sort
Cliff's screenshot after Phase 8: in the combined bag the game's backpack
icon in the top-right corner still covered all of Sort but the "S". It is
GamepadBagBar's BackpackButton, which the client parents to the bag
window and leaves showing without a gamepad (GamepadBagBarMixin:OnLoad
hides its buttons through GetBagButton, which returns nil then). The
Phase 8 fix read that button's left edge to narrow the search box, but in
game the room came out as the plain padding, so the geometry was not
there when the search row was laid out.
Now, while the gamepad interface is off (InputUtil.IsGamepadUIEnabled,
which the client's ContainerFrame code uses), that button is hidden when
Bags sets up Sort, and hidden again on its OnShow (a HookScript, which
catches Show and SetShown alike), wherever the bar sits. The backpack
stays on the bag bar beside the menu. With a gamepad the button is left
to the game. InputUtil added to read_globals.
Review notes, not changed: a gamepad switched on or off mid-session is
followed the next time the game shows the button; with a gamepad the game
hides the search box itself, so the old FitSearch room is kept as is.
Verified (harness only): bagsections.test.mjs, hidden without a gamepad
and after the game shows it, on the bag or another parent; left alone with
a gamepad. README sentence and smoke step 9.5 updated. Needs an in-game
check. - Merge remote-tracking branch 'origin/main' into work/menu-badge-clicks
- P5 AuctionSell: Post says in chat what it sends and what the game answers
In game, Post stayed lit and clicks changed nothing, with no caught
error. The code allows only three outcomes for a click that reaches Post
(the count goes up, a chat line about confirming, or a chat line saying
why not), so either the game asked for confirmation or the click never
reached the button. This makes each click say so.- A click on Post chats "Post: sending stack 1 of 10 (10 at 10c each,
24 Hours)." before the restricted call, so no line at all means the
click never reached Post. - For 3 seconds after a click, chat reports the game's answers:
UI_ERROR_MESSAGE, AUCTION_HOUSE_POST_ERROR / POST_WARNING / SHOW_ERROR,
AUCTION_HOUSE_THROTTLED_MESSAGE_DROPPED, and ADDON_ACTION_BLOCKED /
FORBIDDEN naming Vanguard. A secret value shows as "?". - README and SMOKE-TEST 14.9.2 say what the lines are.
Verified: npm run coverage (1281 tests, 100% lines), quality OK,
luacheck 0 warnings in 137 files. Needs an in-game check: the lines are
what tells us the cause.
- A click on Post chats "Post: sending stack 1 of 10 (10 at 10c each,
- Merge pull request #297 from clifftaylor2325/work/theme-polish
257 theme: absorb stripes are one pixel-exact 8 px tile - 252 cooldowns: refresh-window glow on the Cooldown Manager
Adds Modules/CooldownGlow.lua: an accent glow on a Cooldown Manager icon when its
aura has less than a set share of its time left (30 % default, 10 to 60), off by
default. Hands the duration object to the client's curve where it has one, else
reads plain numbers; with hidden times the glow stays off. Options switch and
slider, /vg cdmglow, README, smoke test 20.1, THEME.md and What's new updated.
Verified: new scripts/tests/cooldownglow.test.mjs (7 cases, plain, secret, curve
and fallback). Full verify and coverage still to run. - 257 theme: absorb stripes are one 8 px tile repeated both ways
The Stripes texture is now 8x8 with an 8 px period and the raid shield bar tiles it
vertically as well as horizontally, so the seams are exact and the stripes don't
shimmer. Test checks the tile size. Verified: npm run verify, npm run coverage (100%).
Needs in-game check: shield stripes on a raid tile at 1080p. - #243 menu: guard the badge OnShow hook
- Merge remote-tracking branch 'origin/main' into work/menu-badge-clicks
- Merge pull request #296 from clifftaylor2325/work/engineering-review-prelaunch
Engineering review: pre-freeze fixes (F1, F2, F3, F4, F6.1, F11) - Merge pull request #295 from clifftaylor2325/work/agent-tooling
Agent tooling: edit-time hook, repo skills, patterns doc, ns annotations - Merge pull request #292 from clifftaylor2325/work/engineering-review-spike
Engineering review spike: what can be done better - #243 menu: make every child frame on a badged menu button pass clicks through
The first fix only switched off the mouse on child frames that reported it on, so the game's NotificationOverlay (a hidden full-button frame at level 100 that shows the diamond) was skipped, and Cliff still could not click the badged buttons in game. Every child frame of a menu button (ours excepted) now has its mouse off and its hit area cut to nothing, and is made so again each time it is shown, out of combat.
The cause is still inferred (the harness has no hit testing); needs an in-game check.
Verified: npm run verify and npm run coverage. - Agent tooling: nine more skills, one per recurring task
Adds .claude/skills/{add-setting,add-slash-command,add-media,
add-game-api,polish-module,fix-bug,release,triage-game-report,
sweep-prs}/SKILL.md beside the four from the first commit, so every
task that comes round more than once (a new switch, a /vg command, an
icon, a harness API, a polish pass, a bug from /vg errors, a release,
a game report to triage, the Monday sweep) has its steps, the files it
touches and what it must not do, each written from the repo's own
scripts and docs. CLAUDE.md's Agent tooling section and README's agent
paragraph list them by group.
Verified: scripts/agent-check.test.mjs and scripts/pkgmeta.test.mjs
pass (.claude stays out of the zip). Docs only; no addon Lua changed. - F1 harness: a secret refuses arithmetic, comparison and field reads
world.secret in scripts/lib/wow-world.lua now sets a metatable whose
every metamethod (index, newindex, call, the arithmetic ones, concat,
len, lt, le) raises "attempt to use a secret value", so the harness
fails the same mistakes the client does instead of only arithmetic and
comparison. issecretvalue and the harness's own readers (the two
abbreviation paths in wow-world.lua, drawn in wow-layout.lua) use
rawget. The JS side's world.secret(n) now emits a call to the Lua
world.secret (LuaSource in lua-emit.mjs) instead of a plain table
literal, so every secret a test hands the addon carries the metatable.
Hand-built { __secret = true } tables in helpers.mjs stay plain.
What type() returns for a real secret number is still unconfirmed; the
batch 1 probe (issue #284) is asked to record it.
Verified: npm run coverage (91 files, 1293 tests, 100%), npm run
quality, npm run test:quick. New case in scripts/lua-engine.test.mjs
covers each refused use. - F2 ci: the full suite skips the quick check's steps and the preview on a pull request
lua-suite.yml takes a checks input (default on) that gates the luacheck
install, the quality scan, the TOC check, luacheck, Lua 5.1 syntax and
the release zip steps. full-suite.yml passes checks off for a pull
request (verify.yml ran the same steps on the same commit) and renders
the UI preview only weekly, on a hand-started run that asks for it, or
on a pull request carrying the "preview" label. release.yml is
unchanged (checks on, preview off). README's CI paragraph says so.
Verified: scripts/workflows.test.mjs (56 tests: the checks input, every
gated step, no run script holds an input expression). The hosted run
itself cannot be checked here. - F3.2 quality: report-only rule for script handlers that are not ns.Guard calls
Adds a third report-only pattern, unguarded-script-handler, to
scripts/lib/code-quality.mjs: a SetScript or HookScript handler for a
frame script (OnEnter, OnShow, a HookScript, ...) that is not an
ns.Guard(where, fn) call, so an error in it never reaches /vg errors.
OnUpdate stays with the unbounded-onupdate rule and OnClick on
SetScript is left alone (secure buttons). The baseline is 163, the
count on main today, so the ratchet only stops new ones.
Verified: npm run quality (163 at baseline), vitest on
scripts/code-quality.test.mjs (54 tests). README and the
check-code-quality.mjs header name the new pattern. - F6.1 tests: every ns.GetModule name is registered and load-time lookups respect TOC order
Reads the Lua as text: ns.Module names are unique, every literal ns.GetModule name resolves, and a top-level ns.GetModule names a module listed earlier in Vanguard.toc (PowerTicks, ComboPoints and PlayerAuras depend on UnitFrames this way). Runs in the quick project. - F3.1 slash: run /vg commands under ns.Try so their errors reach /vg errors
An error inside a subcommand was thrown bare to the game's popup and never kept. handleSlash now calls the handler through ns.Try("slash <cmd>"). Test: a command that errors leaves one entry in ns.errors (core.test.mjs). - F4/F11 docs: refresh STATE.md and CONCERNS.md, definition of done is quality plus coverage
STATE.md now matches ROADMAP.md (Phases 4 and 6 to 10 complete, Phase 5 open, 9 of 12). CONCERNS.md drops the "suite is red on main" entry (resolved in Phase 1) and the stale file sizes, pointing at commands instead. CLAUDE.md: done means npm run quality and npm run coverage, since coverage already runs every test (engineering review F4 and F11, #292). - Agent tooling: edit-time hook, repo skills, patterns doc, ns annotations
The edit-time check: .claude/settings.json runs scripts/agent-check.mjs as a
PostToolUse hook after every Edit or Write; for addon Lua and scripts it
reports the code-quality rules (and luacheck when installed) for that file on
stderr with exit 2, so the agent sees the problem at edit time.
Skills under .claude/skills/: add-module, add-harness-scenario,
fix-visual-issue, ready-pr (the last one runs the built-in code-review skill
before a PR is marked ready). docs/PATTERNS.md shows the canonical shape of a
module, a guarded callback, frame state, a secret-safe read, combat-deferred
work, events and timers, a migration and a scenario, taken from the code.
---@param and ---@return annotations on the ns.* helpers in Core/Events.lua,
Combat.lua, DB.lua, Modules.lua and Ticker.lua (comment lines only, no code
change). CLAUDE.md gets an "Agent tooling" section, README a paragraph,
.pkgmeta ignores .claude.
Verified: scripts/agent-check.test.mjs (4 cases: covered paths, a clean file,
a rule-breaking file exits 2, other input exits 0), npm run test:quick, npm
run quality, node scripts/check-toc.mjs, pkgmeta test. Cliff approved the
.claude/ folder and the patterns doc in the thread (2026-10-12). - Merge pull request #294 from clifftaylor2325/claude/project-thread-plseac
Reskin leftovers: calendar filter and class buttons, done list (#141, #142) - Merge remote-tracking branch 'origin/main' into work/reskin-leftovers
- Engineering review: add tools and practices for agent-written code
- 141 windows: skin the calendar's filter and class buttons; 142 docs: fill in the reskin done list
Calendar: the filter button becomes a flat button and the class-totals and class buttons (1 to 11) icon tiles; each is skipped when the client lacks it. README and smoke test 14.5 updated. RESKIN-PLAN section 11 lists S0 to S7 with PRs #129 to #136. The merged reskin branches are already gone from origin.
Verified: npm run verify and npm run coverage (100%). - Merge pull request #293 from clifftaylor2325/work/p12-launch-readiness
P12 launch readiness: smoke test run order by crash risk - P12 smoke test: run order by crash risk
Adds a run-order block to docs/SMOKE-TEST.md (login, combat, auction Post, windows first). Verified: npm run verify, npm run coverage at 100%, npm run package and scripts/check-zip.mjs pass on the 0.5.0 zip. Nothing seen in game. - Engineering review spike: findings ranked by impact and effort
Research only, no code changed. Covers architecture, errors, DB layer, harness, CI cost, doc drift and the hard rules. - Tick Phases 9 and 10 in ROADMAP: merged in #288 and #289 (#291)
Phase 9 (levelling helpers) merged in #288 and Phase 10 (roll tracking) merged in #289, both on 2026-10-12. Phase 5 stays open until its root-cause error is named. Phase 11 stays open as the cuttable phase.
Verified: the diff touches one file, .planning/ROADMAP.md, and changes only the Phase 9 and 10 checkboxes and their progress-table rows. - Merge pull request #289 from clifftaylor2325/claude/project-thread-56kqrp
Phase 10: roll tracker for /roll results and loot rolls - Merge remote-tracking branch 'origin/main' into claude/project-thread-56kqrp
Conflicts:
scripts/tests/options.test.mjs
- Merge pull request #288 from clifftaylor2325/work/p9-levelling
Phase 9 levelling helpers: quest XP, XP bar tooltip, low-supply warning - Merge origin/main into work/p9-levelling
- Tick Phase 8 in ROADMAP: every window styled merged in #281 (#290)
Phase 8 (every window styled) merged in #281 on 2026-10-12. Phases 9 (#288) and 10 (#289) are still drafts, so they stay open. Phase 5 stays open until its root-cause error is named.
Verified: the diff touches one file, .planning/ROADMAP.md, and changes only the Phase 8 checkbox and progress-table row. - Merge pull request #281 from clifftaylor2325/work/p8-every-window-styled
Phase 8: every window styled (PvP reskin, visual issues #187-#204) - #196 #203 windows: use T.Button's disabled look, drop the half-opacity stand-in
Phase 7 (#282) gave T.Button a disabled look (#257): no hover or press,
the label in the disabled text tier, following OnEnable/OnDisable. The
guild Preferred Play Settings Apply buttons and option button rows with an
enabled() check (Situations' "Stop recording and save") no longer set
half alpha; they are just enabled or disabled. GW.GUILD_APPLY_DIM and the
options window's DISABLED_ALPHA are gone, and so is the RefreshState hook
that re-applied the alpha. Tests now check the label's disabled tier and
full alpha. README and smoke step 14.2.2 say "greyed out" / "disabled
look" instead of "faint".
Verified: npm run verify (88 files, 1260 tests, quality scan OK) and
npm run coverage (100%, 21871/21871). - Merge pull request #286 from clifftaylor2325/work/fixes-and-release-prep
Fixes and release prep: micro menu clicks (#243), safe-mode test, 0.6.0 notes - Merge main (#269, #270, #271, #273, #282) into work/p8-every-window-styled
Options.lua was split into area files (#273): the raid preview rows and
the preview refresh on raid settings move to Core/OptionsFrames.lua, the
"Drag game windows" switch and "Reset window positions" button to
Core/OptionsMore.lua. The options window keeps alpha 1 (opaque) with
main's T.PixelSize. README conflicts: main's text with this branch's
insertions re-applied.
Tests updated for main: the options page row counts (+3 raid preview,
+2 window moving), the window height read as pixel-rounded, and the raid
preview's softened colour read through RF.Soften (the panel colour
changed in Phase 6). - Merge origin/main into work/p9-levelling
- Tick Phases 6 and 7 in ROADMAP: merged in #269 and #282 (#287)
Phase 6 (theme scale and motion) merged in #269 and Phase 7 (control states and panel depth) merged in #282, both on 2026-10-12. Phase 5 stays open: its root-cause Lua error is not yet named, and roadmap criterion 4 needs it. Phase 8 waits for #281.
Verified: the diff touches one file, .planning/ROADMAP.md, and changes only checkboxes and progress-table rows for Phases 6 and 7. - Merge remote-tracking branch 'origin/main' into claude/project-thread-56kqrp
Conflicts:
docs/SMOKE-TEST.md
scripts/tests/options.test.mjs
- Merge remote-tracking branch 'origin/main' into work/fixes-and-release-prep
- P10 rolls: roll tracker for /roll results and (feature-detected) loot rolls
Adds Modules/Rolls.lua: parses the group's /roll lines from CHAT_MSG_SYSTEM through RANDOM_ROLL_RESULT (any language, positional placeholders included), keeps 40, shows the latest round highest first in a small panel that goes 30 s after the last roll. Where C_LootHistory has GetNumItems, GetItem and GetPlayerInfo, it also lists the last 4 items rolled for with each member's choice, roll and the winner; elsewhere that part stays off with no error. Nothing rolls automatically. /vg rolls [clear], options switches, README, smoke-test section 18, layout spot in the three setup presets.
Verified: npm run verify (1174 tests) and npm run coverage (100%). Needs in-game check: see the PR. - Merge pull request #282 from clifftaylor2325/work/p7-control-states-depth
Phase 7: control states and panel depth - Merge pull request #269 from clifftaylor2325/work/p6-theme-scale-motion
Phase 6: theme scale and motion - Merge main into work/fixes-and-release-prep
- #243 menu: let clicks through the game's indicator frames; safe-mode smoke test; 0.6.0 notes
- MenuStrip: the game's child frames on a menu button (its new indicator and pulse) stop taking the mouse, out of combat, so the click reaches the button. Cause is a guess from reading the code; needs an in-game check.
- smoke: with every module switched off the addon logs in, opens its options and reports no error.
- WhatsNewNotes: 0.6.0 lines for the menu fix, situations banner, settings guard, profile parts, mover links.
- Package dry run: npm run package and scripts/check-zip.mjs pass (202 files).
Verified: npm run verify and npm run coverage (100%).
- P9 levelling: quest XP on the tracker, XP bar tooltip, low-supply warning
Tracker rows show each quest's XP (setting Quests > Each quest's XP). The XP bar
hover tooltip lists what is left, percent through, rested, kills to level at the last
kill's XP, and the XP in quests ready to turn in (setting). InstancePrep warns once in
chat when a supply drops under its wanted count, never in combat, re-armed after it
recovers (setting). Stub: GetQuestLogRewardXP takes a quest id, as the client does.
Verified: npm run verify and npm run coverage (100%). Needs in-game check: see PR. - Merge work/p6-theme-scale-motion (main with #270, #271) into work/p7-control-states-depth
- README: Phase 7's Options text kept, with main's new Other addons
entry after it. - options.test: main's new row counts, General one higher for Color
assist (#235).
- README: Phase 7's Options text kept, with main's new Other addons
- Merge main (#270, #271 and others) into work/p6-theme-scale-motion
README: main's new Other addons entry and First-run setup text, with
Phase 6's Pixel-perfect wording merged in. - Merge pull request #283 from clifftaylor2325/claude/project-thread-xoib91
Guidelines: model and effort tiers for sessions and GSD - Tier 1 features: other-addon coexistence, chat group board, quest guidance (#209, #210, #208) (#270)
- #209 coexist: offer to turn off a module another addon also covers
At login a new Coexist module checks C_AddOns.IsAddOnLoaded for the addons
in one table (CX.OVERLAPS: bag, nameplate, action bar, unit frame, raid
frame, click-casting, cast bar, minimap, chat and tooltip addons). Each new
overlap is named once per profile in chat, and the Modules page of /vg
shows an "Other addons" card first, with Turn off <module> (module off,
reload offered) and Keep both. Nothing changes without a click; answers
are kept in the profile.
Options gains O.AddRows(key, rowsFn, first) so a module adds rows to an
existing page from its own file instead of growing Core/Options.lua.
Verified: scripts/tests/coexist.test.mjs (fake addon lists, both buttons,
told once, module already off, client without the call), options, core,
feedback and smoke tests, npm run quality, luacheck 1.2.0.
Needs in-game check: the addons' real folder names on the Forever client. - #210 group board: chat LFM and LFG posts by dungeon, role and level
A new GroupBoard module reads CHAT_MSG_CHANNEL and yell (say and guild when
ticked) through ns.On, skipping secret text or senders before any string is
read, and keeps each sender's latest looking-for-more or looking-for-group
post: kind, dungeon (vanilla's dungeons and raids, short names written by
hand), level range, roles, sender and time. /vg lfg (or the button under
Bars, map, chat > Group board) opens a themed board: newest first, a kind
filter, "Only dungeons for my level" (on), ten rows and "+N more", the
whole line on hover. Posts go after 10 minutes (5 or 20) or past 40.
A click calls ChatFrameUtil.SendTellWithMessage (Forever UI source,
Blizzard_ChatFrameBase/Shared/ChatFrameUtil.lua), which opens the chat box
with "/w <name> <message>" filled in; nothing is ever sent by the addon.
Without that call it falls back to Chat watch's plain whisper.
Verified: scripts/tests/groupboard.test.mjs (parsing, Dire Maul vs the
Deadmines, hidden chat, own lines, channels, replace, filters, level
switch, ages and expiry, templates, fallback whisper, tooltip, options,
module off), npm run quality, luacheck 1.2.0. README, /vg help and
docs/SMOKE-TEST.md 16.2 updated.
Needs in-game check: chat events and two-part names on Forever; whether
the game's group finder lists premade groups (#223) is not used yet. - #208 quests: New mark from a client flag, quest-start pins, the arrow
Decision (a) on the issue: the new-quest list comes from a flag the client
exposes, detected at runtime. A new QuestGuide module looks once for a
QuestClassification or QuestTag entry named for Forever or New (none in
the 1.60.1 UI source today) and, when found, paints "New" in the accent
after the title in the quest dialog (gossip lines, details page), the quest
log and the tracker (QP.Titled/QP.WithMark). Without it nothing is marked.
/vg questdata [questID] prints the flag found and what the client says
about a quest, the open NPC's offers and the tracked quests, for the
in-game check. No community quest data is used.
Pins: QuestGuideMap draws "!" pins on the world map canvas from
C_QuestLine.GetAvailableQuestLines and C_QuestLog.GetQuestsOnMap entries
with isQuestStart (both in the Forever API docs), deduplicated by quest.
None while the game's map has QuestOfferPinTemplate pins
(GetNumActivePinsByTemplate), while Questie is loaded, or with the switch
off. Hover: the quest; click: a map waypoint there. Hooks
WorldMapFrame.OnMapChanged and OnShow under ns.Guard.
Arrow: Shift-click a tracked quest points C_SuperTrack.SetSuperTrackedQuestID
at it (setting: or a map waypoint on GetNextWaypoint, else its spot on this
map; each falls back to the other). The followed quest gets a "> next step"
line from GetNextWaypointText in the tracker.
Verified: scripts/tests/questguide.test.mjs (flag by classification and by
tag, hidden values, marks in all three windows, both arrows and fallbacks,
step line, pins with dedupe, hidden and secret entries, game offers,
Questie, MAX_PINS, late-loading map, questdata, module off), the existing
quest tests, npm run quality, luacheck 1.2.0 (C_QuestLine added).
Needs in-game check: whether Forever exposes a new-quest flag, whether its
map already shows quest offers, the quest arrow and the step text. - #250 whatsnew: lines for quest guidance, the group board and other addons
The three modules this PR adds get a line each in the 0.6.0 notes.
Quest guidance opens the Quests page at its pins switch, the group
board the More page at its open button; the other-addons line opens
nothing, since its card only shows when another addon overlaps.
Verified: whatsnew tests (every line's row exists), npm run verify,
npm run coverage. - #210 checks: allow strat and ony; count this PR's options rows
The spelling check from main (typos) flagged "strat" and "ony" in the
group board's dungeon names: both are how LFG chat shortens Stratholme
and Onyxia's Lair, so they go in _typos.toml with a note. The options
split test pins each page's row count; this PR adds 3 module rows under
Modules, the quest guidance card (6) on Quests and the group board card
(9) on Bars, map, chat.
Verified: typos v1.51.1 clean, npm run verify, npm run coverage,
luacheck.
- #209 coexist: offer to turn off a module another addon also covers
- Phase 5: Auction Post says why it is off, with end-to-end Post tests (#271)
- P5 harness: model the post throttle and the posted slot's lock
With world.ah.throttle on, a PostItem or PostCommodity the client sends
fires AUCTION_HOUSE_THROTTLED_MESSAGE_SENT, makes
IsThrottledMessageSystemReady false and locks the posted bag slot
(GetContainerItemInfo's isLocked now reads the bag entry's locked flag).
world.finishPost(auctionID) is the house taking the stack: the units
leave the slot, then AUCTION_HOUSE_AUCTION_CREATED, BAG_UPDATE_DELAYED
and AUCTION_HOUSE_THROTTLED_SYSTEM_READY fire. Off by default, so every
existing test sees the old always-ready throttle.
Sources: Blizzard_APIDocumentationGenerated/AuctionHouseDocumentation.lua
(PostItem/PostCommodity HasRestrictions, the throttle events are
SynchronousEvent) and Blizzard_AuctionHouseUI/Shared/
Blizzard_AuctionHouseSellFrame.lua (CanPostItem gates Post on
IsThrottledMessageSystemReady and repaints on THROTTLED_MESSAGE_SENT and
THROTTLED_SYSTEM_READY).
Verified: npm run verify (1031 passed) and npm run coverage (100%). - P5 AuctionSell: Post says why it is off, and tests drive it end to end
No Lua error reproduces on the Post path in the harness. A new end-to-end
test (scripts/tests/auctionpost.test.mjs) drives the bag click,
SetPostItem, the price check, Max, Post, the throttle drop, the house
taking the stack and the ready event, to the next stack and the last, on
a commodity and on an item, with ns.errors empty after every step and the
PostCommodity/PostItem arguments asserted. Without the in-game error
text, the root cause stays unnamed; what the trace found and fixed:
- Post went silent when the game's message throttle went busy after the
card was painted (Vanguard's own searches use the same throttle). The
card now repaints on AUCTION_HOUSE_THROTTLED_MESSAGE_SENT as the game's
sell frame does, and a click that still lands while busy says so in
chat ("Can't post yet: waiting for the auction house."). - Every reason Post is off now shows on the count line: no price yet,
the bid not under the buyout, the item not in the bags, under the
vendor price, not enough money for the deposit (new check, as the
game's CanPostItem does), or waiting for the auction house. - A bid went to PostItem in copper on a client that takes whole silver
only; it now rounds up to silver like the price.
AH.PostCheck replaces AH.PostPrice (no other caller). README's Sell
sentence and SMOKE-TEST 14.9.2 updated; one auction test's expected count
line now carries its reason. login-scope.json gains the new file
(the probe, run with --write, scoped it fully; its unrelated core.test
drift was not committed).
Verified: npm run verify (1031 passed, quality OK), npm run coverage
(100%), luacheck 1.2.0 (0 warnings, 114 files).
- P5 harness: model the post throttle and the posted slot's lock
- Merge work/p6-theme-scale-motion (Lean core) into work/p7-control-states-depth
- Core/OptionsWindow.lua: main's after-combat keyboard hand-back
(ns.AfterCombat) with Phase 7's setFocus(nil) on hide. - media.test: the dispel badge letters and the Warn glyph are named
through tables (A.DISPEL_GLYPHS, T.STATE_GLYPHS), so they join the
built-at-run-time list the unused-media scan accepts. - options.test: General has one more row (Color assist, #235).
The taint harness passes on Phase 7's code: press, disabled and focus
state live in weak side tables and its button hooks use HookScript.
- Core/OptionsWindow.lua: main's after-combat keyboard hand-back
- SPIKE docs: code quality checks research and gap analysis (#268)
Adds docs/research/code-quality-checks.md. The top half compares the
research with what the repo already has (luacheck, Lua 5.1 syntax pass,
quality scanner, 100% coverage floor, required "Quick checks" gate) and
lists the real gaps. The bottom half is the general research with sources.
Docs only; no code or config changed. Nothing here was run in-game. - Tick Phase 4 in ROADMAP: lean core merged in #277 and #273 (#285)
Phase 4 (lean core) is merged: #277 and the options split #273, both on 2026-10-12. Phases 5 to 8 stay open until their PRs merge. Only the checkbox and the progress-table row change.
Verified: the diff touches one file, .planning/ROADMAP.md. - guidelines docs: model and effort tiers for sessions and GSD
CLAUDE.md gains a "Usage and cost" section: Haiku for mechanical work,
Sonnet for code, Opus only for unreproduced root causes, harness rules
and protected paths; effort low, medium, high on the same split; lean
briefs, one thread per independent branch, drafts until ready, the
full-tests label only for new API surface, harness changes and
releases, and bringing main in once before a merge.
.planning/config.json sets model_profile to budget and points
claude_md_path at the repo's CLAUDE.md (the .claude/ copy does not
exist). PROJECT.md logs the decision.
Verified: config.json parses, npm run quality passes, typos clean.
Docs and config only, no addon code. - Merge main (Lean core #277, options split #273) into work/p6-theme-scale-motion
Conflicts resolved:- Core/Theme.lua: main's ns.FrameState side tables (vgArrow, vgFader)
with Phase 6's whole-pixel arrow size and eased-out fader. - Modules/MenuStrip.lua: main's side tables with Phase 6's badge role
for the free count and the disabled tier for a disabled glyph. - Media: the eleven unused icons main deleted stay deleted; their
sources are gone from media-sources.mjs (build-media --check passes). - docs/THEME.md, more.test imports: both sides kept.
The taint harness passes on Phase 6's code: its hover and window
state was already in side tables and its hooks use HookScript.
- Core/Theme.lua: main's ns.FrameState side tables (vgArrow, vgFader)
- P8 windows: keep Phase 8's per-frame state in ns.FrameState
Phase 4 (#277) forbids fields on the game's frames. The fields this phase
added move to weak-keyed side tables: vgMovable, vgMoveHandle and
vgDragging (WindowsMove), vgBackdrop and vgTimer (WindowsPvP), vgEmptyNote
(WindowsSocial, WindowsMap) and vgEmptyWatched (WindowsMap). Tests read them
through ns.FrameState; windows-all reads vgWindow the same way, and the
auction-crafting field check no longer needs to skip the moving fields.
SetScript calls left are on Vanguard's own frames (drag handles, the options
header, the talent summary tip).
Verified: npm run verify (84 files, 1210 tests, quality scan OK) and
npm run coverage (100%, 20930/20930). - Merge work/p6-theme-scale-motion (with main) into work/p7-control-states-depth
Brings main's seven merged PRs in through Phase 6. Conflicts resolved:- README: main's Look, Options and First-run setup text with Phase 6
and Phase 7 wording merged in, plus main's Settings guard and What's
new entries. - setup.test: both the one-bar-material test (#234) and main's
export-suggestion test kept.
- README: main's Look, Options and First-run setup text with Phase 6
- Merge pull request #273 from clifftaylor2325/work/p4-options-split
P4 options: split the options pages into area files - Merge remote-tracking branch 'origin/main' into work/p4-options-split
- Merge remote-tracking branch 'origin/main' into work/p8-every-window-styled
Conflicts:
scripts/tests/windows-map.test.mjs
- Merge pull request #277 from clifftaylor2325/work/p4-lean-core
Phase 4 lean core: taint checks in the harness, dead code out, one ticker, CI hygiene - Merge main into work/p6-theme-scale-motion
Brings in #272 #274 #275 #276 #278 #279 #280. Conflicts resolved:- Core/Setup.lua: main's Keep-by-default, scale owner and backup
before a preset, with Phase 6's Pixel-perfect choice (setScale after
S.Backup, a Pixel-perfect hint naming Vanguard's scale, Keep's hint
as a function) and T.OpenWindow. - README, SMOKE-TEST, THEME.md: main's text with Phase 6's preset
names, hover and window motion, and Pixel-perfect merged in. - setup.test: Keep is now the sixth choice (after Pixel-perfect).
T.PRESETS now answers the names before #231 (fel, void, ember and the
older ones) through a metatable, so code reading T.PRESETS.fel (main's
settings-guard test) keeps working; pairs still lists the five.
- Core/Setup.lua: main's Keep-by-default, scale owner and backup
- #255 core: keep the mover-link size watch in a side table
Main's new mover links stored vgLinkWatch on the frame they watch, which can
be a game frame. It now lives in ns.FrameState("vgLinkWatch"). The harness
caught it after main was merged in (5 tests in movers and unlock); both files
pass again. - Merge origin/main into work/p4-options-split
Main's Options.lua changes from the profiles PRs land where the code now
lives: the setting tags (O.Tag) on the bar settings, bar visibility and
module visibility rows go to OptionsBars.lua and OptionsMore.lua; the
rest (O.Tag itself, sizes, What's new, the settings-loss rows, profile
reset confirmation) stayed in Options.lua and merged as is.
The page-count test takes main's counts (General 30, Profiles 14,
Situations 16), read from main itself: the split still builds the same
pages, in the same order, with the same rows. - Merge remote-tracking branch 'origin/main' into work/p4-lean-core
Conflicts:
Core/Movers.lua
Core/OptionsWindow.lua
scripts/tests/unlock.test.mjs
- Merge remote-tracking branch 'origin/main' into work/p8-every-window-styled
Conflicts:
README.md
- Merge pull request #280 from clifftaylor2325/work/first-run-setup
#246 setup: Keep by default, UI scale ownership, backups before changes, reload at the end - Merge remote-tracking branch 'origin/main' into work/first-run-setup
Conflicts:
docs/SMOKE-TEST.md
- Merge pull request #274 from clifftaylor2325/work/whats-new
#250 whatsnew: a What's new window after an update, linking each line to its setting - Merge remote-tracking branch 'origin/main' into work/whats-new
Conflicts:
Core/Options.lua
README.md
- Merge pull request #279 from clifftaylor2325/work/movers-anchors
#247 movers: anchor a frame to another's side, match sizes, and cancel an unlock - Merge remote-tracking branch 'origin/main' into work/movers-anchors
Conflicts:
README.md
- Merge pull request #276 from clifftaylor2325/work/situations-banner
#249 situations: a banner while recording, and settings an override can't hold say so - Merge remote-tracking branch 'origin/main' into work/situations-banner
Conflicts:
README.md
docs/SMOKE-TEST.md
- Merge pull request #275 from clifftaylor2325/work/ruleset-keys
#207 data: key prices, sales and characters by realm, ruleset and side - Merge pull request #278 from clifftaylor2325/work/profile-strings
#248 profiles: hardened profile strings (format version, partial merge, backups, large imports) - Merge remote-tracking branch 'origin/main' into work/profile-strings
Conflicts:
Vanguard.toc
docs/SMOKE-TEST.md
scripts/tests/profiles.test.mjs
- Merge pull request #272 from clifftaylor2325/work/settings-guard
#206 settings: notice and restore settings the game failed to load - P7.5 theme: color-blind-safe states, a Color assist switch and dispel type badges (#235)
- State tokens move off the red-green axis (after Okabe-Ito): good
3fbf9a, warn f0b43c, bad e8663f. T.STATE_GLYPHS pairs each with a
glyph (Check, a new filled Warn triangle, Close) where a state stands
alone as a mark: the pet's mood is now that glyph (10 units) in its
state color instead of a 6-unit dot. Threat, heal and channel colours
keep their own hexes. - Color assist (colorAssist, General -> Look, on reload): T.RGB gives
T.ASSIST's friendly 4a9be8, neutral e8d25a and hostile e0663a.
UnitFrames' reaction color now goes through T.RGB (it read T.TOKENS),
so health bars, nameplates and tooltips that share it follow. - Dispel badges: every debuff icon (A.Dress with debuff) gets a badge
in its top-left corner, half the icon and at least 8 units: an edge
backing at 0.8 and one letter per type (new Media icons DispelMagic,
DispelCurse, DispelDisease, DispelPoison from media-sources.mjs). On
the client's aura engine the backing and letters are handed over with
AddDispelTypeTexture; each letter's color map is clear for every type
but its own, so a hidden type still shows the right letter. On
Vanguard's own icons A.ShowBadge shows it only for a type it can read
(never a secret one). The raid tile's dispellable debuff icon, which
carries the wash, gets the badge with it.
Tests: theme (state hexes, glyph table, Color assist swap through the
General page, health bar color), auras (engine badge maps for Magic and
Curse, size and corner; fallback badge for Poison, none for no type, a
secret or an untyped one; registration counts and the no-style notice
updated), units (pet mood glyphs), options (the new first Look row).
Docs: THEME.md (state, reaction and dispel rows, settings table),
README (Look, raid auras, pet readout), SMOKE-TEST 5.4 and 6.2.
Verified: npm run verify, npm run coverage 100% (19376/19376),
build-media --check. Needs an in-game check: the badges in instanced
combat (hidden types), colorblind simulator screenshots, the pet glyph.
- State tokens move off the red-green axis (after Okabe-Ito): good
- P7.4 theme: optional panel gradient and an aspect-aware icon crop (#259)
- Panel gradient: with panelGradient on (/vg gradient, off by default,
applied at once through T.Refresh) a floating window's fill
(opts.shadow, or opts.gradient) runs from -0.012 to +0.012 OKLCH
lightness bottom to top (T.PANEL_GRADIENT, SetGradient VERTICAL with
CreateColor, alpha kept), about 2.4 %, under 7 of 255 levels per
channel. Smaller panels stay flat. Turning it off repaints one colour
(SetVertexColor replaces the gradient, as the stub now models). - Icon crop: T.Crop(texture, w, h) trims T.ICON_EDGE (0.08) off every
edge and, given a non-square slot, crops the long axis further so the
icon keeps its aspect ratio. Bags, action bars, special bars and the
raid missing-buff marker now crop through T.Crop instead of their own
literal coords. Every slot Vanguard draws today is square, so nothing
passes a size yet. - Skipped: exact-pixel small rounded corners. Nothing in Vanguard is
rounded (panels, slots and controls are square by design), so corner
pieces would have no user; left on #259 for when a rounded element is
designed.
Harness: the stub records SetGradient (already in the 12.1 widget API
list) and SetVertexColor clears it. CreateColor was already a read
global.
Docs: THEME.md (T.Panel row, settings table, Icons), README (/vg
gradient), /vg help line.
Verified: npm run verify (1053 passing, quality OK), npm run coverage
100% (19330/19330). Needs an in-game check: the gradient at 1080p
(banding, and that switching it off restores one colour).
- Panel gradient: with panelGradient on (/vg gradient, off by default,
- P7.3 theme: press feedback, a disabled look and a keyboard focus ring (#257)
A themed button now shows three more states:- Pressed: a shade overlay at 30 % over its fill (T.PRESS) and its own
label one screen pixel lower on every anchor, at once, while the mouse
is held (T.PressButton). A protected button darkens but keeps its label
still. Hiding the button clears it. - Disabled: no hover or press, the label in the disabled tier, its own
colour back when enabled (T.DisabledLook, run from OnDisable/OnEnable
hooks and when a disabled button is painted). - Keyboard focus: T.FocusRing draws a one-pixel accentText ring two
pixels out with a black gap; the options window puts it on the row Tab
marks and drops it on a new page or when it closes.
State lives in weak side tables (pressOf, focusOf, labelBefore), not on
the frames. T.PressOverlay and T.FocusParts expose the parts for tests.
Harness: the stub gains GetTextColor, GetNumPoints and Set/GetFontString
(all in the 12.1 widget API list already). windows-map's fill counter now
ignores hidden textures (the press overlay is hidden at rest).
Docs: THEME.md (T.Button row, T.FocusRing, fades table), README (Look and
layout, Options keyboard), SMOKE-TEST 5.3.
Verified: npm run verify (1051 passing, quality OK), npm run coverage
100% (19308/19308). Needs an in-game check: press, disabled grey and the
focus ring (SMOKE-TEST 5.3).
- Pressed: a shade overlay at 30 % over its fill (T.PRESS) and its own
- P7.2 theme: shadows and glows as one nine-slice texture, behind /vg nineslice (#236)
SetTextureSliceMargins is confirmed in Blizzard's generated API docs
(wow-ui-source, live and classic_era branches,
Blizzard_APIDocumentationGenerated/SimpleTextureBaseAPIDocumentation.lua:
"Enables nineslice texture rendering using the specified pixel
margins"), so it joins scripts/lib/wow-widget-api.lua and the stub
records it. SetTextureSliceMode is not needed: the docs give Stretched
as the default.
With theme.sliceRings on, a shadow or glow is one texture with 16-pixel
margins (a quarter of the 64-pixel art), anchored the same whole-pixel
distance out as the eight pieces; T.SizeGlow moves it. A client without
texture slicing keeps the eight pieces. The setting is off by default
and /vg nineslice flips it for the next reload: the docs don't say how
big the client draws the margins on screen, and a wrong guess would
change every window's shadow, so the in-game check decides.
Verified: theme test for the default (eight pieces), the command, one
sliced shadow and glow with margins and placement, and the fallback.
Full suite passes (1048); npm run quality clean. README, /vg help and
docs/THEME.md updated. Needs an in-game check: /vg nineslice and a
reload, then compare a window's shadow and an action button's glow. - P7.1 theme: inner bevel on buttons and raised panels, one bar material (#234)
Buttons and raised panels get an inner bevel (T.BEVEL): a one-pixel
borderHi line at 35% on the first row inside the top border and a dark
shade line at 35% on the first row inside the bottom. Their sheen moves
to the second row so it never merges with the bevel. A panel can ask
for it (opts.bevel) or leave it out (opts.noBevel); slots keep their own
bevel and leave this one out. Plain and inset panels are unchanged.
One bar material: every bar already fills with T.BarTexture(); a new
test checks it across the whole HUD (34 bars: unit frames, raid tiles,
cast bars, XP and the rest) and that a saved Soft pick reaches them all.
Verified: theme test for the bevel lines, their rows and colours on a
button and a raised panel, and none on a plain panel or a slot; setup
test for the bar material (kept on the full login in login-scope.json).
Full suite passes (1047); npm run quality clean. docs/THEME.md updated.
Needs an in-game check: the bevel reads as a machined edge at 1080p and
1440p. - #246 setup: Keep by default, scale ownership, backups, reload at the end
- Setup opens on Keep my current layout and Keep my current sizes, the
first run too (decision b); the layout for your role is marked
"your role" and the screen preset "your screen" - Damage and tanking's hint says it is the layout for tanks too
- the screen step says who sets the UI scale now (the game's slider or
Vanguard's pixel-perfect scale) and that a preset moves the game's
slider and turns pixel-perfect off; hints wrap to the window's width - before setup's first change in a run (layout or screen preset,
accent or raid style) the profile goes to its rolling backup profile
(ns.BackupProfile from #248), said in chat - Done always ends with a reload offer ("Setup is done."), after the
export question on a first run - Reset profile and Start over ask first, saying what is lost, and back
the profile up; Delete and Copy here say what they lose in a tooltip
Builds on #272 (settings guard) and #278 (profile strings); merges the
guard branch into profile strings, resolving the TOC, smoke-test and
profiles test conflicts (the guard's smoke step is 4.7 here).
Verified: setup, options and profiles tests pass; npm run quality and
the media check OK. Full coverage is run by CI.
- Setup opens on Keep my current layout and Keep my current sizes, the
- P6.9 theme: keep hover and window animation state off the frames (#257)
T.Button can paint the game's own buttons, and a field stored on a game
frame plants taint. The hover parts and the window open animation now
live in weak side tables in Core/Theme.lua instead of button.vgHover and
frame.vgRiser, matching the per-frame state rule Lean core (#277) brings
in. This also stops a clash with the menu strip, which already keeps a
boolean vgHover on its buttons. T.HoverParts(button) reads a button's
hover parts.
Verified: the motion tests now find the hover parts through
T.HoverParts and the fades through the fake fader's recorded groups
(each now notes its owner), so they don't depend on where faders are
kept; a client without animations is checked twice. Motion, more and
frames pass; npm run quality clean. - #247 movers: anchor frames to each other, match sizes, exit without saving
A layout now stores relationships, not only coordinates (Core/MoverLinks.lua,
loaded after Core/Movers.lua).- Anchor to another frame's side: a mover's saved entry may hold
rel = { to, side, x, y }. It is worked out to a spot on UIParent each
time the frame is placed (never anchored to another frame, so protected
frames stay safe), and dependents are placed again when the target is
moved, nudged, reset, rescaled, gets a new default, or changes size
(OnSizeChanged via HookScript, after combat). Self, unknown frames, bad
sides and loops are refused with a reason; a loop in a hand-edited
profile falls back instead of recursing. Dragging or nudging an anchored
frame keeps the link and changes its offsets. - Grow direction follows the anchor: the frame's opposite point is pinned
(anchored to a frame's RIGHT, its LEFT is pinned, so it grows right). - Fallback: point/relPoint/x/y keep the last resolved screen spot (against
the nearest edge); while the target isn't registered (module off) the
frame lands there. Resolved spots are clamped onto the screen. - Size matching: match = , kept live. Only
frames that opt in with ns.MoverSizable may be sized: the cast bars
(width, 80-600) and the XP and reputation bars (width 60-1600, height
2-40). Others, self, unknown frames, loops and sizes out of range are
refused with a reason. A frame's own size (w, h) can be set with the new
"Its width" / "Its height" sliders under General -> Layout; a slider
locks (muted, value can't change) while matched or for a frame that
sizes itself, and hovering it says why and how to unmatch
(W.Slider opts.locked in Core/Controls.lua). - Popover: new rows Anchor to (Screen or a frame), Side (replaces the point
dropdown while anchored), X/Y offsets from the side, Match width, Match
height, and a line saying why a change was refused; grown to 280 x 364.
The selected handle's label shows "Below Player 0, -4" for an anchored
frame. - A real cancel (Core/UnlockBar.lua): unlock snapshots the positions table
(ns.Copy); a bar at the top center offers Save and lock (ns.Lock) and
Exit without saving (restores the snapshot in place, re-places every
mover, locks). Not shown while the game's Edit Mode unlocked the frames
(Edit Mode has its own buttons; closing it keeps changes). Setup's Done
and combat still lock and keep changes. - Situations: a recorded spot now carries rel, match, w and h, and
placeSpot replays them through the setters (ns.RestoreMoverLinks), so an
override can hold anchored and matched frames. - ns.WatchMover is not used; dependents are found by scanning the saved
entries.
Smallest choices taken: a hidden target that is still registered keeps
its place, so dependents stay put (the same spot as the fallback); the
nearest side is picked (and the frame stays where it is) when a target is
chosen in the popover; size matching is limited to cast bars (width) and
XP/reputation bars; the unlock bar does not fade (no THEME.md entry).
README: the Look and layout movers sentences and the Situations bullet.
docs/SMOKE-TEST.md: steps 3.6 (anchor the cast bar under the player
frame, spec change, reload, unit frames off and on) and 3.7 (Exit
without saving).
Tests: movers.test.mjs (layout rects/clamp/nearest side; anchoring,
growth, following, refusals; missing target across logins with Unit
frames off and on, clamping, hand-edited loops; matching live, scales,
refusals, loops; own size sliders lock with tooltip), unlock.test.mjs
(popover anchor/side/offsets/drag/match/refusal/reset; unlock bar exit,
save, Edit Mode), situations.test.mjs (links recorded and applied).
Verified: npm run verify (73 files, 1036 tests passed; quality OK) and
npm run coverage (19428/19428, 100.00%). Needs an in-game check.
- Anchor to another frame's side: a mover's saved entry may hold
- Merge remote-tracking branch 'origin/work/settings-guard' into work/first-run-setup
Conflicts:
Vanguard.toc
docs/SMOKE-TEST.md
scripts/tests/profiles.test.mjs
- #248 profiles: profile strings as a public API
Profile strings now carry a format number (2) and the addon version. An
older string (no format: Vanguard 0.5.0 and older, ForeverUI's FUI1) is
upgraded on import: a module-scope string becomes a partial payload, and
every table the string carries goes through its registered ns.Migrate
(new ns.MigrateTable in Core/DB.lua, which ns.Settings now uses too)
before anything is saved. A string from a newer format is refused with a
reason, and so is another kind of Vanguard string (talents, weights).- Partial strings (scope "parts") merge into the active profile key by
key: the string's values win, local values it doesn't carry stay; a
frame's spot, a situation's override and lists are replaced whole. - Include parts on export: Layout (positions, movers, Edit Mode layout),
Situations overrides, Global settings, and each module with saved
settings. /vg export <part ...> (any case), /vg export pick and a new
"Choose what to export" button open a panel (Core/ProfileExport.lua)
with a switch per part, All/None, Export and Close. Everything on is
the whole-profile string, as before. - Large strings import across frames: Core/Serialize.lua's decoder keeps
its place between calls (checksum, base64 and an iterative reader with
its own stack), S.BUDGET units per frame; ns.StartImport runs the first
step at once, then one per C_Timer.After(0) under ns.Guard, with
"Importing... N%" in the box's title. A failing step ends the import
with a reason. Coroutines were not used: the test harness's count hook
can't run in a coroutine thread. - ns.BackupProfile() copies the profile in use (own values, via
ns.OwnValues) into one rolling "<name> backup" profile before every
import; chat says so. - A whole profile matching an existing profile (deep compare, display
and format keys left out) switches to that profile instead of making
"Imported N", and says so; your own export makes no copy. - A font or bar texture the string names that LibSharedMedia doesn't
have here is named in chat with what is drawn instead.
README's profiles sentences and Commands line, /vg help, and three new
smoke-test steps (4.4-4.6) updated. The literal messages the website
reads from Profiles.lua, Slash.lua, Options.lua and Serialize.lua are
kept.
Verified: 8 new cases in scripts/tests/profiles.test.mjs (old-format
upgrade, partial merge, Include parts and the panel, backup, duplicate
match, missing media, a large string across frames, failures across
frames and without a timer); two existing profile cases and one core
case adjusted for duplicate matching and the second change notice.
npm run verify: 73 files, 1036 tests passed, quality OK.
npm run coverage: 19399/19399 lines, 100%.
- Partial strings (scope "parts") merge into the active profile key by
- #249 situations: editing banner and settings an override can't hold
While a situation is recording, a themed strip (raised T.Panel, accent
border, label role) sits along the top edge of the options window, and a
second one at the top center of the screen while the frames are unlocked:
"Editing as: Raid ยท changes apply only in Raid", with a Stop and save
button (new Modules/SituationsBanner.lua, in Vanguard.toc after
Situations.lua). The button stops recording and saves it, as the page's
Stop recording and save does (chat says what was kept), out of combat, and
the base profile comes back. Choice noted: Stop and save, not discard.
The banners clear on stop (button, page, /vg situation stop, logout), on a
profile change, and the screen one when the frames lock (/vg lock or the
combat lock). They follow SI.StartRecording/StopRecording, ns.Unlock/Lock
and O.Refresh through hooksecurefunc under ns.Guard, and ns.OnProfileChanged.
Settings an override can't hold: Core/Options.lua tags getters with the
setting they read (O.Tag, a weak-keyed map; O.SettingOf reads it) in
setting(), the bar helpers, the theme helpers and visibilityRows; the
Nameplates page's own helper tags too. Core/OptionsWindow.lua gives a
tagged row a tooltip ("Not recorded for Raid: this setting changes your
base profile.") and a muted caption while O.SettingNote says so, which is
while recording and the setting is outside SI.LIVE (SI.Holds). Untagged
rows (modules' own pages other than Nameplates, the Modules switches, grid
and snap) get no note; the page says "most". The Situations page gains a
"What an override holds" card: frames, then each SI.LIVE module in plain
words (SI.HOLDS), then that everything else, including settings that need
a reload, changes the base profile. O.Frame() returns the options window.
README's Situations bullet and docs/SMOKE-TEST.md (16.1) updated.
Verified: four new cases in scripts/tests/situations.test.mjs (banner over
the window and the screen, its button; clearing on lock, combat, profile
change and logout, and a click in combat waiting; the tooltip and dimming
on theme, nameplate, XP bar and action bar rows and none on held ones; the
page's list). npm run verify: 73 files, 1032 tests passed, quality OK.
npm run coverage: 19175/19175 lines, 100%. Not seen in game. - P4 planning: tick CI-07 and CI-08
Dead code (CI-07) and the shared ticker and unit-event path (CI-08)
landed on this branch. The phase itself is ticked when the PR merges. - #253 harness: replacing a game frame's script is recorded too
wow-taint.lua now records addon code calling SetScript on one of the
game's frames ("script replaced: <file>:<line> set <script> on the game's
frame <name>"), judged like a field write: the addon hooks the game's
scripts with HookScript instead. The full suite (80 files, 1117 tests)
finds none left after the Minimap fix. CLAUDE.md and the test guide say so.
This is the harness side of #253's "no SetScript on the game's frames":
a static scan can't tell the game's frames from the addon's, the harness
can. - P6.9 theme: eased button hover and a rising window open (#257, fade and hover half)
Hover: T.Button's instant HIGHLIGHT wash is replaced by a raisedHover
fill over the inset and a one-pixel borderHi line inside the top border,
both clear at rest. T.EaseHover eases them in over T.HOVER_IN (0.1 s) and
out over T.HOVER_OUT (0.13 s) from where they are, with OUT smoothing on
an Alpha animation on the textures themselves (no OnUpdate, nothing on
the button). A disabled button shows no hover; hiding a button clears it
at once; Reduce motion makes it instant. Tabs, rows and raid tiles keep
the 8% wash.
Windows: T.OpenWindow fades a window in over T.WINDOW_OPEN (0.13 s) while
it rises T.WINDOW_RISE (4) screen pixels into its own spot: an instant
Translation drop, then a Translation back with the Alpha, so it ends on
its anchor and so on the pixel grid. It opens at once in combat or with
Reduce motion. T.CloseWindow fades out over T.WINDOW_CLOSE (0.1 s). The
options window and the setup wizard use them.
Every fade now has OUT smoothing. Press feedback and the focus ring are
the other half of #257, in Phase 7.
Verified: motion tests for the hover (tints, one-pixel line, ease in and
out with times and smoothing, disabled, hidden, Reduce motion) and the
window (drop and rise offsets, orders, times, close, combat, a client
without animations), with the fake fader extended for SetSmoothing,
SetOrder and SetOffset; windows-map tests count one visible fill per
button at rest. Full suite passed apart from those four counts, fixed;
npm run quality clean. docs/THEME.md (fades table, T.Button, hover),
README and SMOKE-TEST 5.3 updated. Needs an in-game check: that a
zero-length Translation step holds through the rise, and how the ease
feels. - Merge branch 'work/p4-ci-hygiene' into work/p4-lean-core
Conflicts:
.github/workflows/lua-suite.yml
CLAUDE.md
- Merge branch 'work/p4-bench' into work/p4-lean-core
Conflicts:
README.md
- Merge branch 'work/p4-ticker' into work/p4-lean-core
Conflicts:
scripts/tests/plates.test.mjs
- Merge branch 'work/p4-dead-code' into work/p4-lean-core
- #253 taint: per-frame state off the game's frames, hooks not scripts, after-combat deferrals
The harness's field-write check (#244) listed every field the addon
stored on one of the game's frames across the suite: 45 fields in 26
files (vgSkinned, vgWatched, vgPin*, vgBorder, vgWindow, vgPadded,
vgHoverOf, the menu strip's vgSlot/vgRow/vgInset/vgArt/vgIcon(s)/vgGlyph,
the nameplate's own frame, vgSections, vgSuppressed, vgFader, vgChecked
and the rest). Each now lives in a weak-keyed side table:
ns.FrameState(name) in Core/Events.lua gives one table per name, so
files sharing a name still share the state. The conversion was
mechanical (X.vgField -> vgField<input disabled="disabled" type="checkbox" checked="checked" />, with a file-level local per name);
the nameplate's frame is vgPlate. Movers' pin state uses its own weak
tables, with ns.Repin for the menu strip's re-fit.
Also, from the same checks:- Minimap: the mouse-wheel zoom is hooked (HookScript) and only when the
game has no wheel handler of its own; it no longer replaces the
Minimap's script. - The reload button's secure overlay hung on the button, which made the
options window and the reload dialog protected: in combat neither could
open or close. It now hangs on UIParent, covers its button while both
show out of combat, and is put away (hidden, unanchored) when a fight
starts, coming back after it. - PLAYER_REGEN_DISABLED runs before the lockdown, so ns.OutOfCombat there
ran at once. New ns.AfterCombat always queues: the options window's
keyboard and setup's resume use it.
CLAUDE.md's game rules say so: no fields or SetScript on the game's
frames; PLAYER_REGEN_DISABLED handlers defer with ns.AfterCombat.
Tests: every test reading a moved field reads ns.FrameState(name)[frame];
a minimap with its own wheel handler keeps it; reload overlays are put
away in a fight and the dialog still opens; the pin test reads behaviour,
not fields. The options key test and the range ticker test enter combat
and show frames in the client's order.
Verified: full npm test (73 files, 1037 tests) with no field write or
blocked call left; npm run quality and luacheck (0 warnings) pass.
- Minimap: the mouse-wheel zoom is hooked (HookScript) and only when the
- #254 auras: an aura container with no unit is bound to "none"
Container:SetUnit(nil) on the client's engine used to only turn the
container off and leave its last unit bound. It now binds the null unit
"none" and turns it off, so a container made with no unit (the raid
rows) starts on "none" too.
A raid tile whose unit goes away (the group shrank) used to keep its rows
on the old unit, since RF.Update returned before any module's hook. It
now runs RF.emptyHooks; RaidAuras lets go of the unit there (both rows to
nil, so "none" on the engine) and hides the fallback dispel highlight.
Tests (auras.test.mjs): an emptied tile's rows are on "none", turned off
and show no icon; a tile made with no unit starts on "none"; on the
fallback an emptied tile drops its highlight and its unit. The test file
also marks its CreateFrame wrappers for the harness's ownership (#244). - #244 #245 harness: taint, combat lockdown and secret geometry; smoke on both clients
The headless client now holds the addon to rules the real client enforces
and a line-coverage run can't see (new scripts/lib/wow-taint.lua). A broken
rule is recorded and judged like a caught error: a test fails unless
expectErrors names it.- Ownership: a widget made by Core/, Modules/ or Libs/, or made on one of
the addon's frames (a group header's buttons), is the addon's; everything
else (made before the addon loaded, by the world, or by test code standing
in for the game) is the game's. The caller is read from the stack, looking
through C frames and hooksecurefunc wrappers (stub.transparent). - Field writes: addon code storing a field on a game frame is recorded.
- Combat lockdown: in combat, addon code calling Show, Hide, SetPoint,
SetSize, SetAttribute and the like on a protected frame (Secure template,
PlayerFrame and the other unit frames, __wfgStub.protect, or holding one)
is blocked: nothing happens and ADDON_ACTION_BLOCKED fires. IsProtected
answers as the client does. - Secret geometry (world.secretGeometry "derived" or "strict"): the
geometry getters answer with secrets for a secret-valued bar's fill,
secret text, what is anchored to them, and (strict) every game frame.
The client's order around combat: PLAYER_REGEN_DISABLED fires before
InCombatLockdown() turns true, PLAYER_REGEN_ENABLED after it turns false.
Showing or hiding a frame now runs OnShow/OnHide on its shown children.
A bar's fill is drawn by a secret value's hidden number.
#245: createAddon/loggedIn take a client (FOREVER_CLIENT, RETAIL_CLIENT);
smoke.test.mjs runs load, login, unlock, lock and play once as each client
the TOC declares, and once through a fight with strict secret geometry.
Tests: lua-engine.test.mjs covers ownership, field writes, lockdown, a hook
in front of a protected method, both geometry modes and the OnShow cascade.
Test CreateFrame wrappers are marked transparent. scripts/tests/README.md
documents the rules; README's testing paragraph mentions them.
Verified with the fixes in the next commits: full npm test, quality and
luacheck pass (see the PR).
- Ownership: a widget made by Core/, Modules/ or Libs/, or made on one of
- #207 data: drop AP.HouseKey, which nothing calls now
The sales ledger and prices both take their key from ns.Home.Adopt, so
AP.HouseKey was left uncovered (coverage 99.99%). Removed. - #250 whatsnew: a What's new window after an update, linking each line to its setting
The first login after an update shows "What's new in Vanguard x.y": the notes for
the running version (## Version in the TOC), grouped by module, each line with a
setting a link that opens the options window on its page and marks its row
(ns.Options.Show + JumpTo). A thing that starts off says "(off by default)".
Module defaults are unchanged (owner's decision: new modules start on).- Core/WhatsNewNotes.lua: the notes as plain Lua data (ns.WHATS_NEW), one item per
line with plain string literals so the website can read it later. First entry,
0.6.0, lists what main has gained since v0.5.0 (spellbook panel, raid Width
value, auction tab sizes, setup wizard above the handles, errors in hooks,
clicks and timers kept). - Core/WhatsNew.lua: the window (T.Panel with shadow, centred title, close button,
Escape via UISpecialFrames, a wheel-scrolled list with a soft bottom edge, fade in
through T.FadeIn). Shown once per version per account (VanguardDB.whatsNewSeen),
out of combat, never over setup: a new account (setup owed) marks the version
seen without showing; a new character sees it when its setup ends (Setup.onEnd).
ns.AddonVersion() reads the TOC version (Slash's version() now uses it). - /vg whatsnew (HELP, README Commands) and a What's new button under General >
Layout open the newest notes up to this version again, or say there are none. - README, docs/THEME.md (the fade) and docs/SMOKE-TEST.md (step 1.2b) updated.
Verified: scripts/tests/whatsnew.test.mjs (9 cases: show once, version without
notes, link opens the right page and row, new account, new character after setup,
combat wait, /vg whatsnew and the General button, scrolling, every shipped note's
page and row exist); setup, options, core, theme, motion and smoke tests pass;
npm run verify (74 files, 1037 tests passed; quality OK) and npm run coverage (19232/19232 lines, 100%). Not seen in game.
- Core/WhatsNewNotes.lua: the notes as plain Lua data (ns.WHATS_NEW), one item per
- P6.8 theme: no text role under 10 but a badge role at 9 (#258)
At a pixel-perfect scale a point is a screen pixel, so 9 pt text is 9 px
tall. tiny and auraTimerSmall are now 10. A new badge role at 9 is the
only role under 10, for a count or mark on a tiny badge: the error badge
and the backpack's free-slot count use it. tiny stays the floor for raid
tile text that shrinks with the tile, so the missing health on a 25- or
40-player grid is now 10 px on screen, not 9.
Verified: a theme test checks no role but badge is under 10; the raid
tile and backpack count tests follow the new sizes; the setup layout
tests (raid tiles and menu strip, solo to 40-man) still pass. Theme,
raid, more, errorrail, scenarios and smoke pass; npm run quality clean.
docs/THEME.md role table and the README raid line updated. Needs an
in-game check: small raid tiles and small aura icons at 1080p. - P6.7 theme: accent presets jade, orchid and copper; old names kept (#231)
The presets that read as a class, an item quality or an error are
replaced: fel 5bcb7e becomes jade 4fbfa3, void a47bf0 becomes orchid
b48ae8, ember e0664b becomes copper c8784a. Brass and arcane stay. The
swatches are brass, arcane, jade, orchid, copper.
Decision (a)+(c): fel, void and ember (and green, purple, red) are
aliases for the new presets in /vg accent. A profile stores its accent
as a colour, so one that picked fel, void or ember keeps that colour as
its own hex; nothing is migrated.
Verified: the accent preset test covers the new names, every old alias
and the swatch order; a new test loads a profile saved with the old
ember and keeps it. Unlock, options, more, fights, theme, core and setup
pass; npm run quality clean. /vg accent usage and help, the README and
docs/THEME.md's preset table updated. Needs an in-game check: the three
new swatches next to class colours and item links. - CI-08 core: unit events go through ns.OnUnit, and a check keeps it so
Routed through ns.OnUnit:- Modules/UnitFrames.lua: each unit frame's UNIT_HEALTH/POWER/NAME/LEVEL...
registrations were RegisterUnitEvent calls on the secure unit button with
its own OnEvent. They only drive painting, so they now go through one
ns.OnUnit subscription per frame (its own plain frame, kept on
f.vgUnitEvents); the secure button registers nothing. Handlers now run
under ns.Try like every other event handler. - Core/AuraLegacy.lua: the fallback aura container's UNIT_AURA
(RegisterUnitEvent on its frame, re-registered on SetUnit) is one
ns.OnUnit subscription per container, re-pointed with ns.SetUnits.
Left as it is: RaidAuras' UNIT_AURA on the shared ns.On frame (fallback
only). Measured in the harness (Lua VM instructions, 3-tile raid on the
fallback): an unrelated unit's UNIT_AURA (a nameplate) costs ~88
instructions through the shared frame, and that frame keeps the event anyway
for RaidHealing's raid-wide UNIT_AURA, so filtering RaidAuras alone saves
roughly half of that; per-tile subscriptions would cost a frame per tile and
~150 instructions per tile on every unit change (ns.SetUnits), since a
subscription takes at most three units and a raid has up to 40. Behaviour
would change too: the tile showing you answers to both its raid token and
"player" (RF.byUnit), which a per-tile subscription on one token would drop.
New check: scripts/unit-events.test.mjs (JS, runs in test:quick) scans
Core/ and Modules/ (tokenized, comments and strings ignored) for a frame's
RegisterUnitEvent call, or RegisterEvent of a UNIT_ literal or of a name it
can't read, and fails unless ALLOWED lists the file with its call count and
reason. Only Core/Events.lua is allowed (3 calls: ns.On's shared frame and
ns.OnUnit's subscription frames). A self-test covers what the scan flags and
ignores. scripts/lib/code-quality.mjs is untouched (tokenizeLua imported).
Tests changed: auras.test.mjs reads the fallback container's subscription
instead of its frame's registration; units.test.mjs counts the unit frames'
per-frame subscriptions (and asserts them for UNIT_HEALTH). Verified:
luacheck, npm run quality, npm run test:quick, npm test (75 files, 1033).
Needs an in-game check: unit frames (player, target, ToT, focus, pet, party,
boss) still update health, power, name and level; the fallback aura icons
still refresh.
- Modules/UnitFrames.lua: each unit frame's UNIT_HEALTH/POWER/NAME/LEVEL...
- P6.6 theme: accent text and dim variants made in OKLCH (#230)
T.RGB("accentText") and T.RGB("accentDim") are made from the current
accent in OKLCH (T.ACCENT_VARIANTS: lightness +0.047 and chroma x0.94 for
text, -0.207 and x0.67 for dim), cached per accent, so every preset and
custom hex gets both and they follow /vg accent at once. Brass gives
e6b45b and 8a6a2f, a byte off the research's e6b45a and 8a6a2e.
Colours outside the screen's range are clamped per channel.
Accent-coloured text now uses accentText: setup's selected choices and
captions, the options' choice lists, chat tabs, the auction house's
sorted header, shopping list names and sell totals, bag section headers,
the chat watch toast sender, click casting's letter, the talent
planner's badge and tooltip line, links in chat, the "Vanguard:" chat
prefix, and T.Color with no colour (so S.Color too). Bars, borders,
glows, diamonds and selected fills keep the accent. accentDim has no
user yet; Phase 7's bevel and ornament will use it.
Verified: a theme test for brass, a blue, a clamped red and white, and
T.Color's default; tests that checked accent text now check accentText.
Theme, bagsections, chatwatch, setup, talents and more pass; the full
suite passed before those test updates apart from them. npm run quality
clean. docs/THEME.md and the README's Skin API example updated. Needs an
in-game check: accent text reads brighter than the fill on every preset. - #191 tests: the who list page is a group finder page in the every-window check
#191 registered LFGWhoListFrame as a group finder page, skinned like LFGListingFrame and
LFGBrowseFrame: its Inset gets the panel, the outer finder window has the window's. The
every-window check (P8.2) now lists it with the other two pages.
Verified: windows-all 11/11 (harness only). - #201 windows: the drag area stays out of the game menu's layout; older tests know the move marks
Found when the phase's branches were merged and the full suite ran:- The drag area WindowsMove adds is a child frame of the window. On the game menu (a layout
frame) it is now marked ignoreInLayout, so Layout() never counts it (it has no layoutIndex
either, so the game skips it today; this keeps it out if that changes). - more.test.mjs's game menu case read GetText on every child of the menu; the drag area has none,
so the case now asks only children that have it. - auctioncrafting.test.mjs's "only post-hooks" case compares the professions window's fields
before and after; it now skips vgMovable and vgMoveHandle as it already skipped vgWindow
(Vanguard's own marks, not the game's fields).
Verified: windows-move, more, auctioncrafting and windows-all pass (harness only, not seen in game).
- The drag area WindowsMove adds is a child frame of the window. On the game menu (a layout
- #255 bench: npm run bench, CPU per event under a scripted 40-player raid
npm run bench (scripts/bench.mjs, logic in scripts/lib/bench.mjs) loads
Vanguard in the harness with every module on, forms a TEST-ONLY 40-member
raid out of combat (you are raid1; the 40 tiles are made as the client's
group header would, as RAID_ENV does), starts the pull, and plays a seeded
script: health and power changes, auras gained and lost, casts started and
stopped, members leaving and rejoining, and an OnUpdate frame of 1/60 s for
about every eight events, ending with steps that put the raid back so every
repetition starts alike. Each step is one call into Lua through the C API
(no chunk compiled), timed with performance.now(): one warm-up, then 5 timed
repetitions; per event the median time per step, range and spread. One more
repetition runs with the profiler's sampler added to the state's hook (and
removed after), each event's samples in their own accumulator: Lua
instructions per step for the addon and for the harness, and per module
(addon file) the instructions in its own functions plus a time estimate by
instruction share. Every result is labelled "harness measurement"; the
method note is the script's header comment.
Results are saved to coverage/bench/<commit>[-dirty].json (coverage/ is
already git-ignored). --compare <file> prints the change per event (time and
addon instructions) and the modules that moved most, and says so when the
two scripts differ. --budget <pct> (needs --compare) exits 1 when an event's
median time per step grew past it; off by default. --summary <file> appends
the markdown tables. --events, --reps, --warmup, --seed, --no-profile, --out.
Harness changes:- scripts/lib/lua-profile.mjs: emptyProfile() and useProfile(data), so the
bench keeps each event's samples apart (test in lua-profile.test.mjs). - scripts/lib/wow-world.lua: an aura change refreshes the aura containers
from a list kept as widgets are made, instead of scanning every widget
(2,300 after login) on each change. Same containers, same order; it took
UNIT_AURA from about 23 ms to 1.5 ms per step in the bench. New case in
lua-engine.test.mjs: a container made after an earlier aura change still
refreshes, in creation order, and another unit's does not.
The world already models raid units (raid1..raid40, IsInRaid,
GetNumGroupMembers, GROUP_ROSTER_UPDATE on join and leave), so nothing else
was added to it.
CI: lua-suite.yml runsnpm run bench -- --summary "$GITHUB_STEP_SUMMARY"
after the coverage run, without --budget, so a slowdown never fails it (an
error the addon catches under the raid does); the JSON goes into the
lua-coverage artifact. workflows.test.mjs checks the step. README lists the
command.
Tests: scripts/bench.test.mjs (quick project, under 1 s): options and their
errors, the seeded generator, the script's determinism and that a replay
model finds no impossible step and the closing steps restore the raid,
statistics, event rows, module counts, the printed result and the compare
tables and budget. scripts/tests/bench.test.mjs (lua project, about 8 s):
runs the command with tiny counts, checks the saved JSON, the summary, the
compare heading and that --budget 1 against a very fast earlier result
exits 1.
Verified: npm run quality, luacheck, npm run test:quick, the touched test
files, and the full npm test on 2 workers. npm run coverage was not run
(the lead runs it on the merged branch); no addon Lua changed.
Needs an in-game check: compare the bench's per-event picture with /vg perf
in a raid once.
- scripts/lib/lua-profile.mjs: emptyProfile() and useProfile(data), so the
- P6.5 theme: slate surfaces stepped evenly, raisedHover and borderHi (#229)
The dark surfaces now step about 0.04 apart in OKLCH lightness at one
cool hue: inset 0c0e13, panel 13161c, raised 1b1f26, divider 2f343d. Two
new tokens: raisedHover 262a32 (an opaque control under the mouse) and
borderHi 43484f (the inner bevel and focus base); later tasks use them.
void keeps its role.
Decision (a): defaults only. panel and inset are profile settings and a
profile saves them at its first login, so profiles made before this keep
12151b and 07080b until /vg theme reset; raised, divider and the new
tokens change for everyone.
Verified: a theme test checks the six surfaces get lighter step by step,
that a new profile uses the new panel and that a saved one keeps its
own; the raid softened-colour and text-contrast expectations follow the
new panel. Full suite passed before these two test updates, theme and
raid after; npm run quality clean. docs/THEME.md updated. Needs an
in-game check: options window, bags and quest log, before and after. - #189 docs: smoke steps for the group finder search page, Legacy, the pane and appearances
New SMOKE-TEST steps, each with Do, Expect and paste lines:- 14.5.1: the group finder search (who list) page, its empty line, the
settings gear by the close button, and the Legacy challenge prompt
(#191, #192, #200, #193). - 14.8.6: the Character tab's small square tabs (#189).
- 14.8.7: the pane's flat backdrops, the empty currency line and the
arrow in the title row (#190, #202). - 14.8.8: the appearances backdrop and empty line (#203, #200).
#189 needs no code change. The blank third square tab is
PaperDollSidebarTab3, the Titles tab (Camelot PaperDollFrame.xml; the
order is stats, equipment manager, titles, pet). The game disables it
while the character knows one title or none, and its OnDisable dims it
to half alpha and desaturates its icon. Vanguard does not touch these
tabs (they are not in GW.TabButtons, and the window fade skips child
frames), so the grey tab is the game's own disabled state. 14.8.6 asks
for that to be confirmed in game.
Verified: core 50/50, npm run quality OK. The steps themselves are
harness-free docs, not seen in game.
- 14.5.1: the group finder search (who list) page, its empty line, the
- #202 character pane: the gold back arrow sits in the title row
The gold arrow is CharacterFrame.RightPaneToggleButton (Camelot
CharacterFrame.xml): a plain 28x28 Button with a mixin, not protected,
anchored TOPRIGHT of LeftPaneHost at -6,-6, so below the title row. The
game only swaps its textures (UpdateRightPaneToggleButton) and never
re-anchors it. GW.DressCharacterPane now anchors it CENTER on the close
button, T.CONTROL.step to the left, as GW.PlaceControls does for a
window's maximize box.
Choices: no combat guard, as the button is not protected. Pinning it by
the close button (rather than the left pane's corner) keeps it in the
title row whether the stats half is shown or hidden; when the pane is
collapsed the two places are the same corner.
Verified (harness only, not seen in game): windows-tabs "character
pane: the right-pane toggle (the gold arrow) sits in the title row,
beside the close button" (also tabs off, and no close button);
windows-tabs 26/26, core 50/50, npm run quality OK. - #190 character pane: flat backdrops, inset list panels and an empty currency note
The wood behind the currency list is not TokenFrame's: it is the pane's
own CharacterFrame.LeftPaneHost (UI-Character-Info-General-BG), shared by
every tab, with RightPaneHost's stat backdrop, stone plate and divider
line on the right (Camelot CharacterFrame.xml). GW.DressCharacterPane now
fades both hosts' textures (and their child frames'), puts a flat inset
panel on each pane list's ScrollBox (reputation, skills, currency), and
watches TokenFrame.ScrollBox with the shared empty note:
"You have no currencies yet." while the game's data provider is empty.
Choices: the game has no global string for an empty currency list, so
the text is Vanguard's own. Fading the hosts also flattens the
character tab's own backdrop behind the model, as the rest of the
window is flat. The scroll lines at the top and bottom of each list
are left as the game draws them.
Verified (harness only, not seen in game): windows-tabs "character
pane: the wooden and stone backdrops faded, each list on a flat inset
panel" and "currency: an empty list says so"; windows-tabs 25/25, core
50/50, npm run quality OK. - #200 windows: prompts in the empty Legacy challenge pane and the appearances grid
The Legacy window's challenge detail pane now shows a centred note while
its list is empty: "Select a category to view its challenges." before a
category is picked, and the game's LEGACY_NO_CHALLENGES (or Vanguard's
own "No challenges to show here.") after. The appearances grid shows
"No appearances to show for this slot." when the filtered visuals list
is empty, refreshed on the grid's UpdateItems.
Choices: the game has no global string for either case, so the text is
Vanguard's own English. The note is a theme "label" font string in
"text2", built by the shared GW.EmptyNote / GW.WatchEmpty helpers.
Verified (harness only, not seen in game): windows-map "legacy system:
the challenge detail pane prompts for a category", windows-tabs
"appearances: an empty grid says so"; windows-map 22/22, windows-tabs
23/23, core 50/50, npm run quality OK. - #207 data: key prices, sales and characters by realm, ruleset and side
A new Core/Home.lua (ns.Home) builds the key Vanguard keeps shared data
under: the realm's normalized name, the ruleset when the client names
one (C_Seasons' active season, else C_GameRules' Hardcore rules; neither
confirmed on Forever), and the side, as "Realm-Faction" or
"Realm-ruleset-Faction". Without a ruleset the key is unchanged.- auction prices, the sales ledger and other characters' items use it;
an entry under an older key ("Realm-Faction", or the inventory's
"Realm Name - Faction") moves to it once (ns.Home.Adopt) - two-part names: chat watch's toast leaves your realm off a sender
("Name-YourRealm" shows as "Name"), whispers keep the full name, and
only your name alone or with your realm counts as your own line
(a namesake on another realm now rings) - /vg realm prints the realm, ruleset, side, data key and name format
for the in-game check (help, README Commands, SMOKE-TEST 16.1) - vendor memory is world data kept for the account; no key, no change
Verified: home, chatwatch, inventory, auction and auctionledger tests
pass; npm run quality OK. Full coverage run in progress.
- auction prices, the sales ledger and other characters' items use it;
- #203 collections: flat inset panel in place of the appearances' leather backdrop
The collections window (CollectionsJournal, GW.LATER in Windows.lua, plain
GW.Skin) only fades its own frame's art. The appearances tab's content sits
on WardrobeCollectionFrame.ItemsCollectionFrame, a CollectionsBackgroundTemplate
frame (Blizzard_SharedXML/Mainline/SharedCollectionTemplates.xml: a
collections-background-tile leather tile and shadow pieces on an
InsetFrameTemplate with a Bg and a NineSlice), so the leather stayed. The
sets tab's preview (SetsCollectionFrame.RightInset, same template) and its
list (SetsCollectionFrame.LeftInset, InsetFrameTemplate) are alike.
WindowsTabs.lua now dresses the window's inside through GW.INSIDE
(CollectionsJournal = GW.DressCollections, Windows.lua untouched): each of
GW.WARDROBE_BACKDROPS has its own textures faded, its NineSlice faded, and a
flat inset panel. The models and buttons are its children and stay the
game's. It runs when Blizzard_Collections loads and on every show.
Choice: the toy box and heirlooms grids use the same template but are left
alone; the issue names only the appearances.
Verified: windows-tabs.test.mjs "appearances: the item grid's and the sets
tab's leather backdrops are faded, with a flat inset panel behind the
content" (new, with bare-client cases); npm run quality; core.test.mjs
(README). Harness only, not seen in game. - #192 group finder: empty note on the who list, gear beside the close button
The who list (LFGWhoListFrame, the search view) showed an empty list with no
message, while the browse page says LFG_LIST_NO_RESULTS_FOUND over an empty
result (its NoResultsFound line). The who list now gets a note across the
middle of its list that reads that same game string while its ScrollBox holds
no rows; it is checked now, after each SetDataProvider (the game's
UpdateWhoList calls it on every WHO_LIST_UPDATE) and when the list shows.
The note comes from new shared helpers in WindowsMap.lua (GW.EmptyNote,
GW.SetEmptyNote, GW.BoxEmpty, GW.WatchEmpty), which WindowsTabs.lua uses too.
The browse page's settings gear (LFGBrowseFrame.OptionsButton, set by the
game at TOPRIGHT -12,-32) sat under Vanguard's close button. It now sits
T.CONTROL.step left of LFGParentFrameCloseButton and level with it, as
GW.PlaceControls lines up a window's max/min box; Windows.lua is untouched.
The gear is a plain DropdownButton, not protected, so no combat guard.
Choices: the note reuses LFG_LIST_NO_RESULTS_FOUND as the issue asked, so a
who search that finds nobody reads "No groups found..." (the game has no who
list empty string in its code); it shows before the first search too, since
the list is empty then.
Verified: windows-map.test.mjs "group finder: the who list says the game's
no-results line while empty, and the settings gear sits in the close
button's row" (new) and the extended bare-frames case; npm run quality;
core.test.mjs (README). Harness only, not seen in game. - #191 group finder: skin the who list (search) page like the other pages
The group finder's search view (the one with the who search box and the
Filter button) is LFGWhoListFrame (Blizzard_GroupFinder_VanillaStyle,
Mainline/WhoList.xml), a PortraitFrameTemplateNoCloseButton page that was
never registered, so it kept its brass NineSlice border and round portrait
badge. It is now registered with the other pages (GW.LFG_PAGES) and gets
GW.SkinLFGPage: its template art, border, badge and its own backdrop,
header stone, inside frame and scroll lines faded, and its title in the
heading font. It has no Inset frame, so the inset panel goes on its list
(GW.LFG_WELLS: Inset, else ScrollBox).
Choice: the who page's search box, search and filter buttons are left as
the game draws them (the issue names only the border and badge).
Verified: windows-map.test.mjs "group finder: the who list page (the search
view) loses the brass border and portrait badge..." (new) plus the existing
finder cases; npm run quality; core.test.mjs (README). Harness only, not
seen in game. - #206 settings: notice and restore settings the game failed to load
Vanguard now stamps both saved-variable files at logout and keeps a
backup of this character's profile as a profile string in the character
file. At login a missing or older account stamp (or a missing character
file the account still remembers) means the settings didn't load:- chat says so once, and a question offers Restore (from the backup) or
Paste a string (no backup); Not now leaves it for later - General and Profiles open with a "Saved settings didn't load" card:
Restore the backup, Paste a profile string, Start fresh: run setup - a lost character file picks its old profile again (reload offered)
- the first-run wizard waits while a loss is unanswered; an unanswered
loss keeps the old stamps so the next login asks again - a first run and files from before the guard fire nothing
- after the first Done in setup, a question suggests exporting a
profile string (Skip and a rerun ask nothing)
Vanguard only notices and restores; it can't stop the game overwriting
the files, which stays an in-game check (SMOKE-TEST 4.4).
Verified: npm run coverage 100% (19201/19201; the full suite passed, the
range test rerun alone after a 30 s timeout under CPU load), npm run
quality OK, media check OK. luacheck isn't installed here; CI runs it.
- chat says so once, and a question offers Restore (from the backup) or
- P4 options: split the options pages into area files
Structure only; nothing a player sees changes.
Core/Options.lua (898 -> 367 lines) keeps the shared helpers, now also
on the options table (O.ModuleSettings, O.Setting, O.LiveSetting,
O.Check, O.Append), the General, Modules, Profiles and Feedback pages,
O.MODULE_INFO and the page list. The other pages move, unchanged line
for line, into their own files, each adding its page with O.AddPage:- Core/OptionsFrames.lua: Unit frames and Raid frames
- Core/OptionsBars.lua: Action bars
- Core/OptionsQuests.lua: Quests
- Core/OptionsMore.lua: Bars, map, chat
The page list names those pages by key in their old places, and
O.AddPage puts a page with a listed key in its place (any other page is
appended as before), so ns.Options.PAGES keeps its exact order and the
nav sort, which is not stable for the two order-70 pages, gives the same
result.
General, Modules and Profiles stay in Options.lua on purpose: the
website's generator reads O.MODULE_INFO and several of those pages'
captions from Core/Options.lua by name. Its options-page extractor gives
the same pages, rows and defaults for this tree as for main (checked by
running the site's extractAddon over both).
Core/OptionsWindow.lua (840 -> 743 lines): the General page's accent row
and live sample move to Core/OptionsPreview.lua, which adds the two row
kinds with O.AddRowKind. Search, keys, scrolling and the nav share the
window's locals, so they stay.
Vanguard.toc lists the new files after Options.lua (area files) and
after OptionsWindow.lua (OptionsPreview, which needs O.AddRowKind).
Verified: a dump of every page's rows (kind, caption, current value,
choices, ranges, defaults), the search matches for eleven words and the
built row count were byte-identical before and after. New test in
options.test.mjs: the page list with each page's row count and the nav
order, equal to main's (it passes on main too). npm test (2 workers),
npm run quality, luacheck and the media check pass.
- #193 Windows: game windows, the quest log and the options window are opaque
The theme's panel colour has alpha 0.94 (Core/Theme.lua TOKENS, theme
track), and window-level panels used it or less, so the world showed faintly
through windows, and through the empty space under short options pages
(Talents, Situations). Fixed at the window level only, through T.Panel's
opts.alpha (the fill opacity):- Modules/Windows.lua GW.Skin: alpha = 1, so every game window skinned
through it (the framed windows, popups, breath bars, the on-demand
windows, and every Windows<Area>.lua group that uses GW.Skin) is opaque. - Modules/QuestLogView.lua: Vanguard's quest log, 0.97 -> 1.
- Core/OptionsWindow.lua: the options window, 0.98 -> 1; its fill covers
the whole window, so the area under a short page is opaque too.
Tooltips and menus (T.Tooltip, 0.94) and the theme tokens are unchanged.
Not changed (other files, listed for their owners): window-level panels
still below 1 are Core/Setup.lua (0.98), Core/Feedback.lua (0.98),
Core/Dialog.lua (0.98), Modules/QuestDialogView.lua (0.96),
Modules/GameAlerts.lua:67 (0.92), and panels at the token's 0.94 by default:
Modules/WindowsTown.lua:213, WindowsSocial.lua:151 and :326,
WindowsSystem.lua:98, :260 and :283, WindowsMap.lua:221 and :339,
WindowsTabs.lua:277, AuctionCrafting.lua:213, InstancePrep.lua:254.
Verified (harness only, not seen in game): more.test.mjs "windows are
opaque: game windows, the quest log and the options window (under a short
page too); tooltips keep the theme's" (fill alpha 1 on MerchantFrame,
Vanguard_QuestLog, Vanguard_Options on the Situations page; GameTooltip at
the theme's 0.94); more, options, skins, theme, preview, scenarios and all
windows-*.test.mjs pass. README sentences and SMOKE-TEST 2.1b.
- Modules/Windows.lua GW.Skin: alpha = 1, so every game window skinned
- #203 Situations: Stop recording and save is greyed out while nothing records
The Situations page's "Stop recording and save" button was always live,
even with Record for at Off. Option rows had no way to show a disabled
state, so Core/OptionsWindow.lua gets the smallest support: a button row may
carry a fourth field, enabled(); while it returns false the button is
disabled (Disable(), so a click does nothing) and drawn at half opacity, the
same look the talent planner's Spend planned uses. It is re-read on every
O.Refresh. The Situations row passesSI.recording ~= nil. No other button
row has a fourth field (checked across all 14 pages in the harness).
"Chat background" (Bars, map, chat): not changed. The row is a plain switch
(Core/Options.lua moreRows, setting("Chat", "background")), with no
dependency and no code anywhere that greys an options row (the only alpha
changes in the window are the nav's search dimming, the scroll thumb and the
0.18 accent wash O.MarkRow puts on a row found by search or reached with
Tab). A row looks darker when its switch is off (dark track, muted knob),
under the mouse (hover wash) or when marked by search/Tab; any of those
would match the screenshot. Needs an in-game look.
Verified (harness only, not seen in game): situations.test.mjs "the options
page records, stops, clears and switches overrides off" now checks the
button greyed out (disabled, alpha 0.5) at Off, live while recording and
greyed again after stopping; options.test.mjs and core.test.mjs pass.
README sentence and SMOKE-TEST 2.1a. - #200 Click casting: Add key and its hint at the top of the hover keys card
Why the panel looked empty: the hover-key row kind is a fixed 248 px list
(8 rows of 26 px and the footer), and the Add key capture with its hint (or
the no-snippets note) was anchored to the list's BOTTOM. With no hover keys
every row is hidden, so the card was a tall empty block whose only content
sat at its foot. In the harness at 1920 x 1080 the card's title shows at the
bottom of the page as it opens while Add key sat 158 px below the page's
visible area (the scroll view clips it), which matches the screenshot: a
titled card with no text and no add control. The hint also ran 26 px past
the card's right edge.
Now the Add key capture and the line beside it (the hint, the "+N more"
count or the mouse-button note) are the list's top line, right under the
card's title, and the rows start under it (KEYS_TOP, 32 px). The line beside
Add key ends 8 px inside the card and wraps; the no-snippets note takes the
same top line and wraps inside the card too. The row kind's height is
unchanged.
Smallest choice: the card keeps its fixed height (the options window has
no per-row dynamic height), so the empty rows still leave room below.
Verified (harness only, not seen in game): clickcast.test.mjs "the hover
keys card shows Add key and its hint right under its title, inside the page
as it opens" (Add key 8 px under the title and inside the page before
scrolling, the hint inside the card, a key's row under the Add key line,
the no-snippets note in its place); the rest of clickcast.test.mjs and
core.test.mjs pass. README sentence and SMOKE-TEST 10.1a. - #198 Talents: the plan summary stops short of Apply Changes
The summary font string on the planner's bar had only a LEFT anchor, so a
long line ("30 planned, 9 bought, 21 to buy; next: ...; 1 unreachable (...)")
ran across the window's foot and over the game's Apply Changes button. On
Forever that button (ClassTalentsFrame ApplyButton, 164 x 22) is centred on
the 1212 px TalentsFrame, so it starts at about 524 px, inside the bar's 534.
The summary now has a right end: 12 px (TP.APPLY_GAP) short of the window's
ApplyButton when it has one, else the bar's own right edge. It is left
justified, wraps to two lines at most (TP.SUMMARY_LINES; the client cuts the
rest), and an area over it (mouse motion only, so it takes no clicks) shows
the whole summary in a "Talent plan" tooltip.
Smallest choice: the summary text itself is unchanged (/vg talents prints
the same line); only the bar's copy is limited, with the tooltip for the rest.
Verified (harness only, not seen in game): talents.test.mjs "the summary
stops short of the game's Apply Changes button, the whole of it in a
tooltip" (12 px gap to Apply at Forever's size, the tooltip text, leaving,
the bar's end without Apply, no tooltip for an empty summary); the rest of
talents.test.mjs and core.test.mjs pass. README sentence and SMOKE-TEST 15.5. - P6.4 theme: brighter text tiers and a disabled tier (#227)
text2 is now c8cdd6 (11.5:1 on the panel) and muted a6adb9 (8.1:1, the
old text2). A new disabled token, 7c8390 (4.8:1), is for disabled and
placeholder text only. Moved to it: the search box hint, the auction
house's Relist and Cancel next undercut while they can't be pressed, the
shopping list buttons while unavailable, and a disabled menu bar icon.
Other muted uses (card titles, descriptions, notes, the switch knob, chat
"off") stay muted and get brighter.
Verified: a theme test checks the four tiers' contrast on the panel,
brightest first, and the search hint's tier; the auction and menu bar
tests now expect the disabled tier. Theme, auction, auctioncrafting,
more, fights and options pass; npm run quality clean. docs/THEME.md token
table and icon tint note updated. Needs an in-game check: the options
window and a tooltip still read as a clear hierarchy. - #187 Bags: Sort sits beside the game's backpack button, not under it
On Forever the client parents GamepadBagBar (Blizzard_MainMenuBarBagButtons,
Shared GamepadBagBarMixin:OnLoad) to ContainerFrameCombinedBags at its
top-right corner (-5, -19), and without a gamepad its BackpackButton
(GamepadBackpackButton, 37 x 37 on the bar's right) is left showing. Sort
ended 12 px inside the right edge, so that button covered all but the "S".
BG.FitSearch now reserves the room up to that button's left side plus the
6 px gap when the bar is on the bag window and the button shows; the search
box narrows by that much, so the row reads search box, Sort (full 52 px),
backpack button, with nothing overlapping. With no such button (or a bar
parented elsewhere) the row is as before.
Smallest choice: the game's button is left where the game puts it (it is
the game's own bag slot button); only Vanguard's search box and Sort move.
Verified (harness only, not seen in game): bagsections.test.mjs "the game's
backpack button in the bag's top-right corner sits beside Sort, not over
it" (sections on and off, button hidden, bar on another parent); the rest
of bagsections.test.mjs and core.test.mjs pass. README sentence and
SMOKE-TEST 9.4 added. - P8.2 tests: every reskinned window opened once, its skin found applied
The Phase 8 roadmap criterion "A harness check opens every window Vanguard
reskins and finds the skin applied to each". New
scripts/tests/windows-all.test.mjs reads the registry (GW.ORDER, GW.REGISTERED,
GW.ON_DEMAND), so a window registered later is checked with no change here.
One login per group (GW.GROUPS, ten cases): every window of the group the
stub world lacks is built with WINDOWS_ENV's __window before login; the
on-demand ones are built after login and their parts of the game loaded
(ADDON_LOADED); then each is shown once and must be watched (vgWindow) and
carry a Vanguard texture on itself. A first case checks that the test's group
list matches GW.GROUPS and that every exception names a registered window.
Exceptions, listed in the test with the reason: QuestFrame and GossipFrame
(not hooked: the quest dialog stands in), RaidFrame, LFGListingFrame,
LFGBrowseFrame and PVPRankFrame (pages inside a panelled window),
AddonDialog (the dialog inside the catcher is panelled), SplashFrame (panel on
a child below its art), QuestMapFrame and QuestLogPopupDetailFrame (left to
Vanguard's quest log while it is on), BattlefieldMapFrame (edge and shadow on
its border frame), PVPReadyPopup (its ReadyStatus child), the two widget
containers (a backdrop child, shown only with widgets), TimerTracker (its
bars) and PvPObjectiveBannerFrame (fonts only). The tabs group registers no
window of its own; its case asserts that.
Findings: no registered skin errors on a bare stub window (the error guard
would fail the case). Checked by mutation: with the queue dialogs' panel
removed, the pvp case lists the three dialogs.
Verified: npx vitest run scripts/tests/windows-all.test.mjs (11 cases, about
13 s, full logins: the file is not in login-scope.json, which only the probe
writes); npm run quality. - S8.5 docs: battleground windows in the README and the smoke test
README: the "Battleground windows: (coming: S8)" placeholder is replaced by
what the pvp group does (scoreboard and results, queue dialogs, objective
widgets, the countdown bar, the objective banner's font, the rank details).
docs/SMOKE-TEST.md: section 14.9 gets four steps (14.9.1 queue dialogs,
14.9.2 scoreboard and results, 14.9.3 objectives and the countdown, 14.9.4
honor tab and the switch), each with Do, Expect and paste lines. The #188
auction step that was numbered "14.9.1" and sat after section 15 collided with
the 14.9 the plan reserves for S8: it is now "9.4 Shopping and Selling tabs at
the auction house", moved into section 9 (bags, merchant, trainer) with its
text unchanged. RESKIN-PLAN.md: S8.5 ticked, S8 added to the Done list (PR
number left for the integrator).
Verified: core.test.mjs (smoke-test checklist: every step has its Do, Expect
and paste lines) passes. Docs only. - S8.4 WindowsPvP: the honor tab's rank details pane
S7.3 (Modules/WindowsTabs.lua, skinPvPRank) already fades the honor tab's
decoration (MainInfoFrame's line and the glow behind the ring) under the tabs
switch. What it left is the rank details side pane: PVPRankFrame is now
registered under pvp with GW.SkinPvPRankPane, which fades DetailFrame.Divider
and puts DetailFrame.DescriptionScrollBar (a MinimalScrollBar) through
T.ScrollBar. No panel of its own: the character pane carries it.
Choices: the plan's "rank bar T.StyleBar" does not apply on Forever: the
Camelot PVPRankFrame's RankProgressBarDisplay is a Cooldown swipe (a ring),
not a StatusBar, and S7.3 keeps it as content; nothing is styled there.
Verified: windows-pvp.test.mjs "S8.4 honor tab: the rank details pane's
divider faded ..."; windows-tabs.test.mjs still passes (its PVPRankFrame
fixtures have no DetailFrame); npm run quality. Harness only, not seen in game. - S8.3 WindowsPvP: objective widgets, match timer and objective banner
UIWidgetTopCenterContainerFrame and UIWidgetBelowMinimapContainerFrame
(Blizzard_UIWidgets, UIWidgetContainerTemplate = ResizeLayoutFrame) get a
backdrop frame of Vanguard's filling the container at its frame level (the
widgets sit above it) with a flat HUD panel (T.HudPanel, GW.WIDGET_PAD = 4
units out), shown only while the container has widgets
(HasAnyWidgetsShowing, else a child count), checked on OnSizeChanged and after
UpdateWidgetLayout. The backdrop has ignoreInLayout = true: BaseLayoutMixin
would otherwise count it as a child and the container could never shrink.
TimerTracker (Blizzard_FrameXML Timer.xml): each StartTimerBar's StatusBar
(bar) gets the theme's bar texture, its backdrop and border faded (the bar
texture kept), a flat HUD panel and the time text in the theme font at the
game's size; once per bar, for the bars there at login and after each
TimerTracker event. The big countdown digits stay the game's.
PvPObjectiveBannerFrame (Mainline PVPBanner.xml): Title and TitleFlash in the
"title" role, BonusLabel in "label", each at the size the game gave it
(T.TextSize), colours left alone; its art stays (its animation sets the alpha).
Choices: the plan said hooksecurefunc TimerTracker_OnEvent, but the frame's
OnEvent is bound from XML (function="TimerTracker_OnEvent") at load, so a hook
on the global would never run: TimerTracker:HookScript("OnEvent") instead.
The banner gets fonts only (no panel), as the plan says. No new fade.
Verified: windows-pvp.test.mjs "S8.3 widget containers: a flat panel behind
the widgets ...", "S8.3 widget containers: without the game's mixin ...",
"S8.3 the match timer: ...", "S8.3 the objective banner: ..."; npm run
quality. Harness only, not seen in game. - S8.2 WindowsPvP: battleground queue dialogs
PVPReadyDialog, PVPRoleCheckPopup and PVPFramePopup (Blizzard_LFGUtil,
Mainline PVPHelper.xml, loaded on Forever) are registered as "popups" and get
GW.Skin hugging the dialog: their own art and Border (DialogBorderTemplate)
faded, a flat panel with a shadow, the close button themed and their named push
buttons (EnterBattle/LeaveQueue, Accept/Decline) themed through GW.AddButtons.
Content art is kept through GW.CONTENT_ART: the ready dialog's battleground
picture (background) and the invite's battleground icon and ring. The invite's
hide button (minimizeButton) becomes a window control with the Minimize glyph.
PVPReadyPopup (PvpPopup.xml) draws itself on its ReadyStatus child, which has
no parentKey: its children with a Border are skinned as dialogs, so the panel
sits on ReadyStatus, not on the popup.
Choices: the ready dialog's UIPanelHideButtonNoScripts (it hides the dialog)
is dressed as the close button GW.Skin finds by name. Role buttons keep their
art (role icon and check). Every frame named in S8.2 exists on Forever.
Verified: windows-pvp.test.mjs "S8.2 queue dialogs: ..." and "S8.2 the ready
popup is drawn on its ReadyStatus child; the group off ..."; npm run quality.
Harness only, not seen in game. - S8.1 WindowsPvP: battleground scoreboard and match results
PVPMatchScoreboard and PVPMatchResults (Blizzard_PVPMatch, confirmed in the
Forever source: Content/content, TabContainer/tabContainer, ScrollBar/scrollBar,
earningsArt, requeueButton/leaveButton, SetupArtwork) get the window skin
hugging the frame (pad 0: the room the border art had is inside the frame),
the table's backdrop and inset border faded with a flat inset panel behind the
rows, the tab strip's edge lines faded, the results' reward band faded (its
OrangeGlow/OrangeSmoke burst kept: it is an animation), the scroll bar through
T.ScrollBar and Queue again / Leave as flat buttons. SetupArtwork (which lays
the faction NineSlice on the frame) is hooked once so the new art is faded.
Choices: Blizzard_PVPMatch is LoadOnDemand 0 on Forever (loads at login), but
it is registered "onDemand" so a client that loads it later is covered too.
"Header buttons" read as the window's push buttons: the table's column
headers are text-only (PVPHeaderStringTemplate) and stay the game's, as do the
rows and the team colour wash (ScrollBox.Background). Tabs are left to
WindowsTabs.lua (it dresses frame.Tabs on every registered window).
Verified: windows-pvp.test.mjs "S8.1 scoreboard and match results ...",
"S8.1 scoreboards: art the game lays on later ...", "S8.1 scoreboards: loaded
later (on demand), switched off, or bare"; npm run quality. Harness only, not
seen in game. - CI-08 core: one throttled ticker (ns.Every) drives the per-frame pollers
New Core/Ticker.lua (in the TOC after Core/Combat.lua): ns.Every(key,
seconds, fn[, frame[, now]]) runs fn(key) everysecondsfrom one driver
frame, whose OnUpdate is set only while some job is registered;
ns.Every(key, nil) stops a job. A job tied to a frame counts time only while
that frame is visible (as the frame's own OnUpdate did);nowruns a new job
on its first visible frame (the old countdown drivers did). Asking again for a
running key keeps its count. Each run goes through ns.Try under
"ticker <key>" (or the frame's name). ns.ticker is the driver frame and
ns.ticker.vgJobs the jobs by key, for tests.
Moved onto it (each had its own OnUpdate elapsed accumulator):- ActionBars range poll (no range events), RaidStatus range poll (follows
the raid header's visibility instead of a hidden ticker frame), UnitFrames
target-of-target refresh, InfoStrip clock/system/rates, QuestTracker
nearest-first sort, QuestTrackerView item range tint (per button, runs on
its first frame), Nameplates mouseover watch (NP.hoverWatch frame replaced
by NP.WatchHover), Minimap coordinates, AfkScreen clock, Errands flight
poll (ER.poller frame removed), MainTanks tile poll, Waypoints world-map
coordinates line (WP.OnMapUpdate/WP.wait replaced by WP.PaintMapLine, runs
on its first visible frame).
Left on their own OnUpdate (genuinely per-frame): - Core/Controls.lua slider track: follows the cursor during a drag.
- Core/OptionsWindow.lua resize grip: follows the cursor during a drag.
- Modules/PowerTicks.lua spark, Modules/SwingTimer.lua rows,
Modules/CastBars.lua bars (plain-times path): animation every frame while
shown; they stop themselves. - Modules/AuctionHelper.lua scan walker, Modules/AuctionUpgrades.lua gear
judge: spread a batch of work over consecutive frames (no accumulator).
Tests: scripts/tests/ticker.test.mjs (new) covers the idle cost (no OnUpdate
with nothing registered; a 1 s job runs once per second over 64 and 1,280
frames, once for one 5 s frame), frame-tied jobs,now, re-asking, stopping,
a job stopping another mid-frame and failure reporting. Tests that drove a
private accumulator now drive the clock or read ns.ticker.vgJobs:
plates.test.mjs (mouseover watch via addon.update), more.test.mjs (quest
tracker sort, flight poll). Verified: luacheck, npm run quality, npm test
(74 files, 1031 cases).
Needs an in-game check: CPU idle cost (/vg perf) and that each poller still
updates (ToT, raid range fade, minimap and world-map coordinates, info strip,
AFK clock, flight timer, main tank tiles, quest item range tint).
- ActionBars range poll (no range events), RaidStatus range poll (follows
- P6.3 theme: texel snapping policy by texture class, behind /vg texelsnap (#226)
T.Snap(texture, class) now puts every texture under one policy, kept in
T.SNAP_CLASSES. With texel snapping on, the still classes (border, fill,
divider, sheen) snap to the pixel grid with bias T.SNAP_BIAS (1); the
moving ones (bar, spark, swipe) and art (icon, the default) stay off.
Decision (b): the setting (theme.texelSnap) is off by default, so nothing
changes until the in-game A/B test. /vg texelsnap [on|off] flips it at
once for that test; T.Refresh re-applies it, so a profile saved with it on
snaps from login. Theme borders, fills, sheens, the tab line, hover washes,
bars and sparks are classed, as are the menu strip divider, the quest
tracker rule and the raid tiles' top line.
Bias 1 is the smallest reasonable pick of the research's 0.5 to 1 range
that still sharpens; the A/B test may lower it.
Verified: theme tests for both paths (off by default, on, toggle, off, a
texture made while on, a saved profile at login); the harness stub now
records the bias. Theme, core, raid, smoke, status and more pass; npm run
quality clean. README command list, /vg help and docs/THEME.md updated.
Needs an in-game check: the A/B screenshots at 1080p and 1440p, at
pixel-perfect and fractional scales; bars and sparks never step. - #265 ci: PR template, SHA-pinned actions, spelling and Markdown lint
(a) .github/PULL_REQUEST_TEMPLATE.md with the sections CLAUDE.md asks a
pull request for: Tasks, Tests added, Skipped, Needs in-game check.
(b) Every action in .github/workflows is pinned to a full commit SHA with
its version in a trailing comment (Dependabot's monthly github-actions
update moves both). Looked up with git ls-remote on each action's repo
(api.github.com is limited to this repository from this session), with
annotated tags dereferenced (^): actions/checkout v7 -> v7.0.1
3d3c42e5, setup-node v7 -> v7.1.0 949feb24, upload-artifact v6 -> v6.0.0
b7c566a7, github-script v9 -> v9.0.0 3a2844b7 (annotated), crate-ci/typos
v1.51.1 7f9d0819 (annotated), DavidAnson/markdownlint-cli2-action v24 ->
v24.2.0 21c1be1b (annotated). Each pins the commit its major tag points at
today, except typos, whose v1 tag points at a commit no release tag
names, so the latest release tag is pinned.
(c) Spelling: typos with _typos.toml (excludes Libs/, Media/, art/ and the
lock file; ignores WoW |T texture escapes; lists the WoW words and code
names it flagged, each with why). It runs as the "Spelling" step of
verify.yml right after the docs-only check and without its skip, so
docs-only pull requests are spell-checked too. Run locally with the
v1.51.1 release binary: 56 findings before the config, none a real
misspelling (two-hander, defensives, Varian, Lok'tar, IsSpellOverlayed,
test names, mis-sized, PNGs), so no source was changed; clean after.
(d) Markdown lint: .github/workflows/markdown.yml, its own small
non-required workflow (job "Markdown lint"), path filter docs/**, **.md,
its config and itself; drafts skipped; DavidAnson/markdownlint-cli2-action
pinned. .markdownlint-cli2.jsonc keeps the defaults on and turns off only
the rules the docs break on purpose (13 rules, each with its reason:
unwrapped lines, compact tables, bare URLs, continued numbering, bold
labels, tight lists and headings, plain fences, placeholders in angle
brackets, spaces in code spans, merged table cells); .planning/ (generated
notes) is ignored. No doc was edited. Run locally with markdownlint-cli2
0.23.2 (the action's version): 0 issues in 19 files.
New top-level files (_typos.toml, .markdownlint-cli2.jsonc) are in
.pkgmeta's ignore list. verify.yml's header and README's CI paragraph say
all this.
Secret scanning and push protection are repository settings and were not
touched.
Tests (scripts/workflows.test.mjs): every non-local uses: is
owner/repo@<40-hex> # vX.Y.Z; one action has one pin everywhere; verify's
third step is the pinned typos action and _typos.toml excludes Libs/,
Media/, art/ and lists the WoW words; markdown.yml's path filter, job
name, draft skip, pinned action and read-only permissions; the
markdownlint settings keep defaults on and comment each rule turned off;
the template's four headings. The docs-only test now expects the spelling
step to run unconditionally. Verified: workflows, pkgmeta and check-zip
tests, npm run quality, luacheck, typos and markdownlint as above. - P6.2 theme: whole-pixel sizes and offsets, Pixel-perfect screen preset (#225)
Sizes and offsets now land on whole screen pixels. T.PixelRound rounds a
length to whole pixels at a frame's scale (even counts for centred things);
T.PixelSize sizes a frame that way and remembers it. Panel padding, glows,
sheens, the close and window controls, arrows, the logo, unit frames, the
options and setup windows and the action bars' button sizes and gaps use
them. T.Rescale sizes them again on the next frame, out of combat, when the
UI scale, the screen size or a mover's scale changes, and calls T.OnScale
listeners (the action bars lay out again).
Setup's screen step gains "Pixel-perfect (sharpest)" beside the four
presets (decision (c)): it leaves the game's UI scale alone, turns on the
pixel-perfect scale at a whole multiple (pixelMultiple: 1, or 2 at about
1600 pixels tall and up), and grows the detected preset's buttons and text
to make up for the smaller scale. Raid tiles keep the preset's size: grown
tiles made the party extras and raid overlap at 1080p. The setup window is
30 taller for the sixth choice.
Verified: theme, setup, bars, auras, windows-tabs, more and the rest of the
suite in the headless harness, including the layout test for every
layout at 1080p, 1440p, 4K and ultrawide with Pixel-perfect; tests that
expected raw unit sizes now expect them in whole pixels. npm run quality
clean. The new setup case keeps the full login (login-scope.json).
README, docs/THEME.md and docs/SMOKE-TEST.md updated. Needs an in-game
check: sharpness at 1080p, 1440p and 4K. - P6.1 media: white under clear pixels, a clear pixel inside every icon's edge (#228)
Shapes drawn by scripts/lib/media-art.mjs started from transparent black and
over() wrote RGB 0 into pixels that stayed clear, so a tinted, filtered icon
edge could blend toward black. image() now starts transparent white and over()
leaves a clear pixel's colour alone. Every icon, the diamond and the logo halves
are regenerated with white RGB under alpha. The Profiles icon's shoulders and
the menu cog's teeth reached the canvas edge; they now stop a pixel short.
Tests: scripts/media.test.mjs checks that every tinted image is white in every
pixel (Icon.tga is pre-coloured, Shadow one colour, the bars opaque), and that
icons and stand-alone shapes have no alpha on their outer ring. Both fail
against the old generator. build-media --check passes. docs/THEME.md updated.
Needs in-game check: tinted nav and minimap icons at 1080p, zoomed. - #201 windows: drag game windows by their title bar, places remembered
The game places its panels itself every time one opens: ShowUIPanel hands
the frame to the forbidden FramePositionDelegate, whose
UpdateUIPanelPositions clears each open panel's points and anchors it to
UIParent by area (left/center/right, UIPanelWindows attributes), and the
bags are stacked from the bottom-right corner by
UpdateContainerFrameAnchors. So a window dragged with StartMoving alone
snaps back the next time it opens or another panel opens.
New Modules/WindowsMove.lua (listed in Vanguard.toc after the other
Windows*.lua files, using GW): each window in GW.MOVABLE that the client
has (now, or when its part of the game loads) is made movable and clamped
to the screen, out of combat, and gets an invisible drag area over its
TitleContainer (the game menu: its Header), stopping 40 units short of the
right end so the close and minimize buttons stay clickable; a window with
no title bar gets a 24-unit strip along its top, clear of the portrait.
On drop its top-left corner is saved in UIParent units (scale taken out,
since the game rescales the bags) and SetUserPlaced(false) keeps the
client's layout cache out. The saved place goes back on OnShow and after
UpdateUIPanelPositions and UpdateContainerFrameAnchors (hooksecurefunc),
always through ns.OutOfCombat (keyed per window), and never while the
window is being dragged. No drag starts in combat (CollectionsJournal
holds SecureFrameTemplate buttons, so moving it in a fight is blocked).
Settings: the Windows module's profile settings getmovable = trueand
moved = {}(window name -> { x, y }). Options, Bars, map, chat -> Game
windows: a "Drag game windows by their title bar (reload)" switch and a
"Reset window positions" button. Vanguard's options window (already
draggable by its header) now remembers its place under
moved.Vanguard_Options, is clamped to the screen, and reset puts it back
in the middle at once.
Smallest choices: the switch applies on reload like the other window
switches; reset forgets the places and each game window returns to the
game's place the next time it opens (calling UpdateUIPanelPositions from
addon code would taint the panel manager); a window the game places
during combat waits at the game's spot until the fight ends. The list adds
friends/social, merchant, mail, professions, achievements and the
transmog window to the ones the issue names. Left to the game: world map
and its quest panel (the quest log keeps its own mover), loot and roll
windows, popups, the ready check, flight map, Edit Mode.
Verified (harness only, not seen in game): windows-move.test.mjs, 6
cases: drag saves the point and it comes back after the panel manager
re-anchors and on re-show, left alone mid-drag; combined bag strip and
place kept across a rescale; nothing moves in combat (drag refused,
put-back and setup of a window loading in combat queued until it ends);
switch off leaves windows alone; options switch, reset and the options
window's remembered place (also after a fresh login); a client without the
windows or layout calls loads cleanly, game menu by its header, broken
saved data ignored. WindowsMove.lua 100% lines covered by that file.
options, core, smoke and windows-* tests pass; npm run quality passes. - #194 #195 #196 #193 guild: wider Rank column, readable offline rows, info note, faint Apply
Guild window fixes in Modules/WindowsSocial.lua, each checked against
Blizzard_Communities in the Forever source first.- #194 roster. The columns come from tables local to
CommunitiesMemberList.lua (GUILD_COLUMN_INFO: Rank header 85, Note 0 =
the rest), and each row's RefreshExpandedColumns sets Rank back to 75.
The only non-tainting route is post-hooks (hooksecurefunc): on the
roster's ColumnDisplay:LayoutColumns (the guild Rank header +40, found by
its attribute "guildRankOrder" in the column table the game passes),
and on CommunitiesMemberListEntryMixin's RefreshExpandedColumns (Rank
cell 115; a fixed-width Note beside an extra column 93 -> 53; a filling
Note shrinks by itself). The mixin hook covers rows made later; rows made
already get the hooks one by one. Other club types and the collapsed
chat-side list keep the game's widths.
Offline dimness is the game's own: UpdatePresence colours every text of
an offline row DISABLED_FONT_COLOR (0.5 grey). After it, offline rows are
painted with the theme's text2. Smallest choice: text2, not muted, as
muted (#6b7280) is no brighter than the game's grey; text2 (#a6adb9)
reads on the dark panel and stays unlike online rows (white text,
class-coloured names). - #195 Guild Info. The box is GuildDetailsFrame.Info.DetailsFrame's scroll
child .Details, filled by CommunitiesGuildInfoFrame_UpdateText (on show,
GUILD_ROSTER_UPDATE and after an edit) with C_GuildInfo.GetInfoText();
blank when the guild has no information. Vanguard adds its own font
string there ("No guild information yet.", text2), shown only while the
game's text is empty, after each UpdateText; the game's string is not
touched. - #196 Preferred Play Settings. The game does disable the Apply buttons for
non-guild-masters (RefreshState: SetEnabled(isGuildMaster and ...)).
WindowsSocial did not theme them before; they are now flat T.Button
buttons drawn at alpha 0.5 while disabled, again after each RefreshState.
T.Button has no disabled look of its own (a theme item, reported). - #193 (guild part). GuildPreferredPlaySettingsFrame has no art or inset
of its own and was not in GW.GUILD_PANES, so nothing was painted behind
it. It is now a guild pane: an inset panel on the page itself.
The privateafterhelper is now built onafterWith, which passes the
hooked call's own arguments (needed for the mixin and global hooks).
README "Mail, guild, channels..." bullet and SMOKE-TEST 14.2.2 updated.
Verified (harness only, not seen in game): windows-social.test.mjs new
cases "guild roster: the Rank column wider...", "guild roster: other
clubs' columns...", "guild roster: an offline member's row...", "guild
info: a note in the empty Guild Information box...", "preferred play
settings: the page panelled, its Apply buttons flat and faint...", "guild
pages: with the group off..."; windows-social 26/26 (100% line coverage
of WindowsSocial.lua from this file alone), windows-tabs 21/21, core
50/50, npm run quality OK.
- #194 roster. The columns come from tables local to
- #204 raid: preview the party and raid tiles with sample players
A preview shows the raid tiles filled with a fixed sample roster, so you
can check size, style and layout without joining a group. Turn it on with
"Preview with sample players" at the bottom of the Raid frames page (Off /
Party (5) / Raid (10) / Raid (25) / Raid (40)) or /vg preview
[party|10|25|40|off] (on its own, it toggles a raid of 10).- Modules/RaidPreview.lua (new, in the toc after RaidStatus.lua): plain
insecure frames (Vanguard_RaidPreview, Vanguard_RaidPreviewTile1-40),
dressed with the raid tiles' own parts and painted with their painters.
They are laid out as the group header lays out the real tiles (groups of
five as columns or rows, sorted by group or role, width, height and
spacing from the settings, scaled by RF.Scale for the size when compact)
and pinned to the raid holder at the header's point. 40 made-up players,
with classes, roles and missing health; the first group (the party
preview) has the one dead and the one offline player. - It never touches the secure header or its buttons, and never starts in
combat. It ends on PLAYER_REGEN_DISABLED, on GROUP_ROSTER_UPDATE once
IsInGroup() is true, and when turned off, each with a chat line. It
won't start in a group or with the raid frames off. It is module state,
not a setting, so it is off after a reload. - Modules/RaidFrames.lua: small exports only. RF.Dress (panel + parts),
RF.PlaceTexts, RF.HeaderPoint, and painters taking plain values that the
unit-based updates now call: RF.PaintRole, RF.PaintStatus,
RF.PaintColors, RF.PaintTopLine, RF.ShowPower / RF.PaintPower,
RF.DeficitText. Behaviour is the same, except that the top line now reads
the unit's class color on a full update in every look (before, only in
the softened look). - Core/Options.lua: a Preview card on the Raid frames page. Width, Height,
Sort by role and Groups as now redraw the preview at once (they still
ask for a reload for the real frames), and Style and Power bar redraw it
too. - Core/Slash.lua: /vg preview, with a help line. README (Raid frames
section and the command list) and SMOKE-TEST 6.4.
Choices made: - Party (5) uses the raid tiles, as the party sees them with "Use them
for your party too" on (the default). The separate party frames
(UnitFrames, used with that switch off) are not previewed. They are
secure unit buttons made by a local builder in Modules/UnitFrames.lua,
and an insecure copy would need new exports there, outside this task's
files. - The preview ignores showPlayer (always five in a party), and draws no
aura, heal, aggro or status marks (no setup or update hooks run on
preview tiles). - A holder moved while the preview shows is followed, but a holder whose
side changes (left, right, center) is picked up on the next redraw.
Verified (harness only, not seen in game): raid.test.mjs "raid preview:
sample players in the raid tiles, laid out by the raid frame settings, and
the secure header left alone" and "raid preview: a real group or combat
ends it; it won't start in a group, in combat or with the raid frames off;
not saved"; raid, core, options, frames, auras, healing, status and clicks
test files pass; npm run quality passes.
- Modules/RaidPreview.lua (new, in the toc after RaidStatus.lua): plain
- CI-07 media: remove eleven unused icons
The options window's first page icons (General, Modules, UnitFrames,
RaidFrames, Healing, ClickCasting, ActionBars, Nameplates, Tooltips,
Profiles) and the Move icon were still made and shipped, but nothing has
drawn them since the nav moved to the Nav* silhouettes. Their shapes go
from scripts/lib/media-sources.mjs (with arrowHead, which only Move used,
and the unused frame import), and npm run media removed the files.
npm run media only wrote files, so a removed source left its image
behind and --check passed. scripts/build-media.mjs now deletes images
no source makes when it writes, and --check fails on them
(orphanMedia). New tests in scripts/media.test.mjs:- "no image is left over from a removed source" (orphanMedia is empty);
- "every image in Media/ is used by the addon": the reverse of the
existing named-media scan, so an image the addon stops naming fails.
The scan now also reads icon names passed to T.Icon(x and "A" or "B"),
icon(), W.IconTexture() and T.WindowControl(); the five images whose
names are built at run time (LogoRight, Shadow, Glow, Maximize,
Minimize) are listed with the reason.
The crisp-stroke checks now use icons that ship (Spellbook, Professions,
Quests, Collections; the two-pixel check reads the Quests stroke).
Checked: the website build reads only Media/Fonts; README.md and
docs/API.md name no icon. Docs: THEME.md and the README command list say
that npm run media deletes left-over images; THEME.md no longer cites the
removed icons; one comment in Core/Theme.lua (T.Icon's examples) no longer
names General.
Verified: npm run media, node scripts/build-media.mjs --check, media,
pkgmeta and check-zip tests, npm run test:quick and npm run quality pass.
- #264 tooling: a pre-push hook that runs the quick checks
The owner picked a plain git hook. .githooks/pre-push (sh, LF, mode 755)
runs npm run test:quick, npm run quality and the TOC check
(scripts/check-toc.mjs, well under a second), then luacheck and the
luac5.1 -p pass over every file the TOC loads when those tools are
installed. It runs every check and fails if any failed, prints one line
naming each skipped tool ("luacheck (not installed)", "luac5.1 (not
installed)"), and on Windows (Git Bash: uname MINGW/MSYS/CYGWIN) always
skips luac. git push --no-verify skips it once; CI stays the gate.
npm's prepare script runs scripts/install-hooks.mjs, which sets
core.hooksPath to .githooks. It does nothing when CI is set, outside a git
checkout, or when git fails, so npm install and npm ci never fail over it.
(A Node script rather than a baregit configin package.json, so it
behaves the same under cmd.exe on Windows.)
.githooks is in .pkgmeta's ignore list (the owner approved the new
top-level folder). CLAUDE.md's Commands block says so on the npm install
line; README gains a short "Pre-push hook" paragraph.
Tests: scripts/githooks.test.mjs (9 cases): the prepare script; the hook
is a #!/bin/sh LF file, executable on disk and 100755 in git, and runs the
three checks; it names its skips; run with stand-in tools on PATH (POSIX
only): every tool present passes and runs luac per TOC file, missing tools
are skipped and named, Windows skips luac, a failing check fails the push
after the rest ran; the install sets core.hooksPath in a scratch repo and
leaves CI and a non-git folder alone. Verified: those tests, pkgmeta and
workflows tests, npm run quality, and the real hook run end to end here
(all green; 44 s on this shared 4-core machine with other workers busy,
37 s of it test:quick, so the 15 s target is for an idle machine). - #263 quality: report-only rules for combat guards and OnUpdate, with a baseline
Two heuristic rules join scripts/lib/code-quality.mjs as report-only
(reportSource, apart from checkSource, so the existing rules and their
results are unchanged):- secure-outside-combat-guard: SetAttribute, the state and attribute
drivers, the SecureHandler calls, unit watch and override bindings,
called in no function that runs under ns.OutOfCombat or checks
InCombatLockdown() / ns.InCombat() before the call. "Runs under
ns.OutOfCombat" means passed to it, or named in its arguments, or named
by a function already known to run there (same file, to a fixpoint).
Top-level calls run at load and pass. Strings (restricted snippets) and
comments are not read. SECURE_ALLOW is the explicit allow-list; it starts
empty. ns.Soon is not counted as a guard: it defers to the next frame
without checking combat. - unbounded-onupdate: a SetScript/HookScript("OnUpdate", fn) handler (a
literal, a guarded literal, or a function of that name in the file) that
neither adds up or counts down its elapsed parameter and acts past a
threshold, nor returns early on GetTime(), in a file that never clears it
(a nil handler, also "cond and fn or nil").
scripts/code-quality-baseline.json holds the counts on this branch
(secure 6: Core/Client.lua 4, Core/Visibility.lua 2, both guarded by their
callers in other files; OnUpdate 2: the auction scan and judge walkers,
which stop by hiding the frame). npm run quality prints both counts against
the baseline, says when one can be lowered, and fails only when one rises
above it; --report lists each finding. README's code-quality paragraph
says so.
Tests: scripts/code-quality.test.mjs, 16 new cases: each secure call kind,
deferred by literal, by name and through a call, combat checks before and
after, load time, strings and comments, other folders, the empty
allow-list; OnUpdate every-frame, elapsed and countdown and GetTime
throttles, a sum without a threshold, named handlers, clears, HookScript,
guarded literals, other scripts; the baseline comparison (above, below,
missing); the committed baseline's shape; and npm run quality passing on
the repo with both counts printed. Verified: the test file, npm run quality.
- secure-outside-combat-guard: SetAttribute, the state and attribute
- CI-07 lint: drop an unused read global and duplicate entries
.luacheckrc read_globals listed GetThreatStatusColor, which no addon file
reads (the test client's stub for it goes too), and listed
ClearOverrideBindings, GetBindingKey, SetOverrideBindingClick and
UnitLevel twice; the second copies go.
How it was checked: luacheck run on Core/ and Modules/ with no config
lists every global the addon reads or sets (W111-W113); each name in
globals and read_globals was compared with that list. Seventeen more
names are not in it because the addon reads them through _G or a lookup
by name (AUCTION_EXPIRED_MAIL_SUBJECT, AddonCompartmentFrame,
CHAT_FRAME_TEXTURES, EventRegistry, FCF_Tab_OnClick, GetAddOnCPUUsage,
GetAddOnMemoryUsage, GetCreatureDifficultyColor, GetFunctionCPUUsage,
GetMerchantItemID, PixelUtil, SetCVar, UIErrorsFrame, UnitCastingInfo,
UnitChannelInfo, UpdateAddOnCPUUsage, UpdateAddOnMemoryUsage); each was
grepped and is still read, so they stay. Every name in globals is set by
the addon or the TOC.
Verified: luacheck clean, npm run test:quick passes. - #262 ci: docs-only pull requests report the required Quick checks
The owner picked option 1, "job decides". verify.yml loses its
workflow-level paths-ignore, which left the required "Quick checks" pending
on a docs-only pull request (merged only by an admin bypass). The job now
always runs: the checkout fetches two deep (the merge commit and both
parents), and the first check, scripts/docs-only.mjs, diffs the merge
commit against its base (first parent, --no-renames so a move lists both
paths). When every changed file is under docs/ or ends in .md it writes
docs_only=true to GITHUB_OUTPUT and every later step is skipped by its if,
so the job reports green in seconds. When git cannot list the files the
answer is false and every check runs. Drafts are still skipped by the
job's if, as before. The docs-only step runs on the runner's own Node,
before setup-node, so a docs-only run does no install at all. The header
comment and README's CI paragraph say so.
full-suite.yml keeps its paths-ignore: it is not a required check, and a
docs-only pull request has no need of the whole suite.
Tests: scripts/docs-only.test.mjs (7 cases): which paths count as docs,
an empty list runs everything, merges built in a scratch git repo (docs
only, docs plus code, a code file moved into docs/), a parentless commit,
and the command's GITHUB_OUTPUT line. scripts/workflows.test.mjs: no path
filter on verify, checkout two deep then the docs-only step with id
changes, every later step carries the skip condition, the draft if stays,
and the header explains it. Verified: npx vitest run on both files,
npm run quality. The hosted run itself (that a docs-only PR shows a green
required check) can only be confirmed on a real pull request. - CI-07 core: remove six test-only functions
Removes the six functions only tests called, with the helpers that only
they used:- ns.GridLineCount (Core/Grid.lua). The grid tests in unlock.test.mjs and
the Edit Mode case in more.test.mjs now count what the player sees: the
visible 1 px textures that span the screen (gridLines in helpers.mjs). - ns.WatchMover (Core/Movers.lua), with its watchers table and changed();
no production code registered a watcher. Its movers.test.mjs case goes. - O.RowCount (Core/OptionsWindow.lua). The options rebuild case now counts
the rows the open page shows, and checks that a rebuilt page's old rows
are no longer refreshed (fails when forgetRows is skipped: checked). - W.Cycle (Core/Widgets.lua), with its local nextChoice and refreshCycle
(W.NextChoice in Controls.lua stays and keeps its test). Its part of the
key-capture case in options.test.mjs goes. - CC.Move (Modules/ClickCast.lua). Its assertions in clickcast.test.mjs go;
the export/import check now reads the binding at its unmoved key. - GW.SkinWith (Modules/Windows.lua). The registry case in more.test.mjs
passes an inline skin (GW.Skin then GW.FadeParts) instead.
Grepped the addon, scripts, README.md and docs/API.md for each name
first: only tests used them, none is in the Skin API or a slash command.
Notes added to docs/handoff/CLICKCAST-FINISH.md and RESKIN-PLAN.md where
they named CC.Move and GW.SkinWith as available.
Verified: unlock, movers, options, clickcast and more test files pass;
npm run quality and luacheck clean.
- ns.GridLineCount (Core/Grid.lua). The grid tests in unlock.test.mjs and
- #261 ci: check the TOC against the files on disk
Adds scripts/check-toc.mjs. It fails when a .lua file under Core/ or
Modules/ is not listed in Vanguard.toc (an orphan nothing loads, not even
the harness or the coverage floor), when a file is listed twice (compared
without case, backslashes read as slashes), when a SavedVariables or
SavedVariablesPerCharacter name is missing from globals in .luacheckrc,
when ## Interface lacks 16001 or 120007 (one constant, EXPECTED_INTERFACE),
when ## Title or ## Version is missing, or when a Libs/ file is listed
after one of the addon's own files. Each message names the file and the fix.
verify.yml and lua-suite.yml run it as a step right after the code-quality
scan (plain Node, well under a second, so the gate's one-minute budget
holds). README's CI sentence and both workflow header comments say so.
Tests: scripts/check-toc.test.mjs (12 cases in the quick project): the
repo's TOC passes and the command prints OK; each failure kind; an
unlisted Modules/X.lua fails both the pure check and the command, shown on
a minimal checkout copied to a temp folder (no stray file in the repo).
scripts/workflows.test.mjs checks both workflows run the step after the
scan. Verified: npx vitest run on those files, npm run quality. - Merge pull request #242 from clifftaylor2325/work/gsd-planning
Phases 1-3: green baseline, errors fail tests, fast suite and CI gate - docs(phase-3): mark phase 3 complete, advance to phase 4
- docs(phase-3): regenerate verification, status passed
- test(03): UAT 4 passes on the hosted run after the preview fix
Suite / UI preview passed in 3m6s on PR #242 (run 38196119168); suite job 4m31s, still under 5 minutes. All six UAT items pass; gap G-03-4 resolved by b094212. - 03 preview: build every UI preview scene in the Lua harness
The preview job needs Playwright, so a scene that cannot build only showed up in CI. The scenes and the code that logs the addon in to one move to scripts/lib/preview-scenes.mjs; preview-ui.mjs imports them. scripts/tests/preview.test.mjs builds each of the 37 scenes headless and checks the addon caught no error. With the chat tab font removed 37 of 38 cases fail with the CI error; with it, all pass.
Verified: npm run verify (tests + quality) and npm run coverage (19058/19058). WR-01 recorded as fixed in 03-REVIEW-DISPOSITION.md. - 03 login-scope: a scoped brand-new account is made current, so Core/DB.lua does not migrate it (WR-01)
modulesOffLua created VanguardDB = for a firstRun login with no saved variables of its own; Core/DB.lua read that as a version-0 file and ran MigrateDB(0, 2) instead of a fresh install. It now creates , read from Core/DB.lua. A new test compares migrations and version for a scoped and an unscoped brand-new account; it fails without the fix. - docs(phase-3): record WR-01 (fix) and IN-05 (wontfix) dispositions
- 03 preview: give the scene font strings a font so the UI preview job builds
Suite / UI preview failed on PR #242 with 'FontString:SetText(): Font not set'. Since 6290690 the stub refuses text on a CreateFontString with no font, and seven font strings in scripts/preview-ui.mjs (the chat tab, the damage meter, quest tracker and merchant window mocks) had none; the prelude failure took all 37 scenes down.
Verified: every scene's Lua now builds headless (37 of 37, Playwright rendering skipped, not installed locally); npm run test:quick passes. Needs a hosted Suite / UI preview run to confirm the screenshots render. - test(03): complete UAT - 5 passed, 1 issue
- test(03): record UAT 5 pass
- test(03): record UAT 2 pass
- test(03): record UAT 1, 3, 4 from PR #242 hosted runs
Test 1 and 3 pass (one real run each after the skipped draft run; suite job 205 s). Test 4 is an issue: the toolchain pins work but Suite / UI preview fails with Font not set. - test(03): persist human verification items as UAT
Phase 3 verification is human_needed: 3 of 3 roadmap criteria verified
locally; hosted-runner checks and two decisions remain. Adds 03-UAT.md
and 03-VERIFICATION.md. - docs(phase-3): code review and disposition
0 critical, 1 warning, 5 info. All findings recorded as open in the
disposition ledger; the gate is advisory. - docs(phase-3): add/update security threat verification
Security audit of phase 3 (ASVS level 1, block on high): 25 of 25 threats
closed, threats_open 0. T-03-01 recorded as an accepted risk. - docs(phase-3): add/update validation strategy
Nyquist audit of phase 3: no gaps, every task has an automated test that
runs green (72 files, 989 tests). Hosted-runner checks stay Manual-Only. - docs(03-07): complete phase gate and 2-worker timing plan
- 3-07-02 ci: record the 2-worker suite time
Replaces the unmeasured 'about five minutes on a hosted runner' sentence in
lua-suite.yml's header comment with the measured 2-worker local proxy time:
155.5 s and 158.7 s plain, 170.7 s with coverage, on 2026-10-11. The comment
says the hosted time is not measured yet. Comment text only.
Verified: three runs on a quiet machine (max concurrent files 2, within the
300 s budget, coverage 19058/19058 100.00%); npm test -- scripts/workflows.test.mjs
40 passed. - docs(03-06): complete whole-suite scoped logins plan
- 3-06-02 docs: scoped logins, the probe, timing and profiling
scripts/tests/README.md: a Scoped logins section (the six modules, the data
file and how it is written, FULL_FILES and FULL_CASES, VG_LOGIN_SCOPE=full and
probe, what to do when a new case needs a heavy module, the vacuous-pass hazard
and why coverage is the check) and a Timing and profiling section (npm run
timing with --workers, --budget, --baseline; npm run profile -- <area> and --
login, coverage/profile/, one sample = 1,000 Lua instructions). README.md's
command list names npm run timing and npm run profile -- <area>.
Verified: the four greps of the task's verify block find their lines, and
npm run test:quick passes (231 tests). - 3-06-01 tests: scope every login file from the probe; 907 of 907 phase-start cases pass
scripts/tests/login-scope.json is now the output of
node scripts/login-scope-probe.mjs --write --workers 4over the whole suite
(57 login files, none hand-edited). Probe totals: cases 786, keep-full 147,
scoped 639. No timed-out case in either probe run (0 "timed out" in the report).
Per-file keep-full counts (research's count in brackets; counts include the
hand-kept cases): bars 32 (33), units 15 (15), prep 11 (11), setup 8 (7),
auctionrestock 6 (6), auras 6 (6), core 5 (5), more 5 (5), clickcast 3 (3),
frames 3 (3), visibility 3 (3), masque 2 (2), options 2 (1), clicks 1 (1),
raid 1 (1), range 1 (1), report 1 (1), lua-engine 0 (1). scenarios, smoke and
movers stay whole-file full (FULL_FILES). The setup count is higher by the
hand-kept cases below; lua-engine's one case passes scoped here.
Reviewed FULL_CASES (cross-module checks; D-06). Re-ran the planning grep over
case titles and read core, setup, auras and bars:- core "client capability probe: every flag off, every module still enables"
(kept from 03-04) - setup "the setup wizard sits above the unlock handles on the last step ..."
(kept from 03-04) - setup "setup layouts: nothing overlaps or leaves the screen ..." (added: it
checks the rects of every module's mover against each other, and an overlap
check passes vacuously when a module's frames are missing) - setup "setup screens: with each screen preset, nothing overlaps ..." (added,
same reason) - units "threat and happiness only where the client and settings allow" (added
for coverage: it is the only case that runs Modules/UnitExtras.lua:46, and it
passes vacuously with UnitFrames off because nothing is built)
Not added: auras "raid tile zones never overlap" (RaidFrames.Zones geometry
only, one module) and bars "action bar layout" (one module's own layout; the
probe keeps it full anyway because it needs ActionBars). FULL_FILES unchanged.
scripts/login-scope.test.mjs gains two guards, written first (the data one
failed until the probe wrote the file): every FULL_CASES title is still in its
file, so a rename cannot silently move a hand-kept case onto a scoped login;
every FULL_CASES entry is on its file's keep list in the committed data and no
FULL_FILES path is a key of it.
Verified (4 workers, each log under coverage/p3-*.log): - VITEST_MAX_WORKERS=4 npm run coverage: Tests 989 passed (989), total
19058/19058 100.00%. The first coverage run with the data was 19057/19058
(UnitExtras.lua:46), which is the entry above; floor not touched. - npm run timing -- --workers 4 --budget 900 --baseline 03-BASELINE-CASES.txt:
"baseline: 907 of 907 cases present and passed", "tests 989 passed, 0 failed,
0 skipped", total wall 101.5 s; the 82 new cases are all in quick-project
files this phase added (login-scope 27, lua-profile 8, suite-timing 14,
workflows 33). - A second probe run reproduces the committed JSON (diff prints nothing).
- npm test -- scripts/login-scope.test.mjs (27 passed), npm run quality OK.
No addon Lua, test case, setup.mjs, error-guard.mjs or lua-harness.mjs changed.
- core "client capability probe: every flag off, every module still enables"
- docs(03-05): complete toolchain pins plan
SUMMARY with the luacheck digest evidence and the open hosted checks;
STATE.md and ROADMAP.md plan progress after plan 03-05. - 3-05-03 ci: setup-node reads .nvmrc (Node 24)
The four actions/setup-node steps (two in lua-suite.yml, one each in
verify.yml and release.yml) replace node-version: 22 with
node-version-file: .nvmrc and keep cache: npm. full-suite.yml has no
setup-node step. New cases in scripts/workflows.test.mjs count the steps
per file, require the key and the cache on each, and forbid a node-version
key in any workflow.
Verified: both new cases failed before the edit; npm test --
scripts/workflows.test.mjs scripts/pkgmeta.test.mjs 42 passed; npm run
quality OK; npm run test:quick 229 passed. Not verified (hosted run needed,
assumption A6): node-version-file on setup-node@v7, and Node 24 with the
Playwright preview job. - 3-05-02 ci: .nvmrc for Node 24; pin fengari to exactly 0.1.5
.nvmrc holds 24 (the single source for the CI and local Node major) and is
in .pkgmeta's ignore list so it stays out of the CurseForge package.
fengari goes from ^0.1.5 to 0.1.5 in package.json and in the lock's root
entry, hand-edited (one line each, no npm install). New cases in
scripts/workflows.test.mjs pin the .nvmrc content, its .pkgmeta entry, the
exact fengari version in both files and the lock's resolved entry, and
that vitest stays exact.
Verified: the two new cases failed before the edit; npm test --
scripts/workflows.test.mjs scripts/pkgmeta.test.mjs 40 passed; npm ci
--dry-run --ignore-scripts exit 0; git diff --numstat shows 1 1 for each of
package.json and package-lock.json; npm run quality OK; npm run test:quick
227 passed. - 3-05-01 ci: luacheck 1.2.0 release binary, pinned by a verified SHA-256
verify.yml and lua-suite.yml now install luacheck from the official v1.2.0
release binary instead of apt, only after sha256sum -c accepts the pinned
digest d68da17fca0697d9e2fb04201f3884abd259fa558b3a449bccaed47f1390defc.
lua5.1 stays an apt package (luac5.1 -p). In verify.yml the download and
check run in the background tools group whose wait fails the step; in
lua-suite.yml the && chain is the last thing in the step script. Both print
luacheck --version before running it. scripts/workflows.test.mjs holds the
digest as a constant and fails unless both workflows pin exactly it, check
the order (check before sudo install) and forbid the apt luacheck package.
Digest evidence (2026-10-10):- fresh download of v1.2.0 luacheck: 835048 bytes, sha256sum -c against
research's digest: OK, exit 0 - the same check with the last hex digit changed: FAILED, exit 1 (fails closed)
- luacheck.asc: signer key 89793084D5E649FAB8B61C65B538286DE04ECFE5 (primary
9F377DDB6D3153A48EB3EB1E63CC496475267693, Caleb Maclennan); the public key
was fetched from keys.openpgp.org (by fingerprint) and from
github.com/alerque.gpg (the release author, alerque) and imported into a
throwaway home; gpg --verify: Good signature (key not in a web of trust,
so gpg warns it is uncertified) - release API digest field for the asset: absent (null)
Verified: npm test -- scripts/workflows.test.mjs (36 passed; 5 new cases
failed before the edit), npm run quality OK, npm run test:quick 225 passed.
Not verified: the binary running on ubuntu-latest (assumption A3) needs a
hosted run.
- fresh download of v1.2.0 luacheck: 835048 bytes, sha256sum -c against
- docs(03-04): update state and roadmap after plan 03-04
- docs(03-04): complete scoped logins and probe plan
- 3-04-02 tests: probe that derives which cases need a heavy module
node scripts/login-scope-probe.mjs runs Vitest with VG_LOGIN_SCOPE=probe (every
loggedIn login scoped), reads the JSON report with caseList, and prints for each
test file that calls loggedInkeep-full <n> of <cases>, then the totals. A case
that fails with the six modules off needs one and keeps the full login. With
--write (whole suite only; refused with file filters or when a login file ran
no case) it writes scripts/tests/login-scope.json from buildScope: files and
keep lists sorted, so the same results give the same text. Vitest's exit status
is ignored (failing cases are the result); the probe exits 1 only without a
report. No npm script, package.json untouched.- scripts/lib/login-scope.mjs: buildScope, probeRows, dataText; the probe's own
test file joins FULL_FILES (its logins test the scoping rules under each
mode, so the data must not scope them) - scripts/login-scope-probe.mjs: the CLI (--write, --workers, file filters)
- scripts/login-scope.test.mjs: 6 more tests (buildScope, probeRows, dataText);
the buildScope and probeRows ones failed first with the functions missing
Verified: login-scope tests 25 passed. Dry run on auction and bars:
keep-full 0 of 74for auction.test.mjs,keep-full 32 of 35for
bars.test.mjs (research: about 33); login-scope.json unchanged afterwards
(git diff --exit-code exits 0); --write with a file filter refuses naming the
whole suite. The Vitest JSON report gives the nested case the key
scripts/login-scope.test.mjs|outer > inner, the same shape loggedIn looks up
from expect.getState().currentTestName. npm run quality OK; test:quick 220
passed.
- scripts/lib/login-scope.mjs: buildScope, probeRows, dataText; the probe's own
- 3-04-01 tests: scoped logins from data; auction runs without the six heavy modules
loggedIn now appends modulesOffLua(scopeFor(...)) after the saved variables it
builds, so a case in a file listed in scripts/tests/login-scope.json logs in
with ActionBars, FrameAuras, PlayerAuras, InstancePrep, UnitFrames and
RaidTools off (VanguardDB.profiles.Default.modules.<Name> = false), unless the
case is on that file's keep-full list. A file not in the data, a login outside
a running case and VG_LOGIN_SCOPE=full are today's full login (the saved
variables text is unchanged); VG_LOGIN_SCOPE=probe scopes every login; any
other value throws. createAddon stays the only factory; setup.mjs, the error
guard and the harness are untouched, no expectErrors marker added.- scripts/lib/login-scope.mjs: HEAVY, FULL_FILES, FULL_CASES, SCOPE_FILE,
modulesOffLua, scopeFor - scripts/tests/helpers.mjs: runningCase (file and key from expect.getState),
loggedIn change, scope data read once - scripts/tests/login-scope.json: seeded with auction.test.mjs only
- scripts/login-scope.test.mjs: 19 quick tests (rules, case-name format with a
nested describe, a real scoped login, a brand-new-account shape, merging into
the caller's saved variables, loggedIn under each mode)
Verified: login-scope tests 19 passed (failed first with the module missing);
auction.test.mjs 74 passed scoped and with VG_LOGIN_SCOPE=full; smoke,
errorrail, core 58 passed on the full login; npm run quality OK; test:quick
214 passed.
Auction area on 1 worker (indicative, other work may share the machine):
before (full login) total wall 27.9 s, after (scoped) total wall 15.0 s.
- scripts/lib/login-scope.mjs: HEAVY, FULL_FILES, FULL_CASES, SCOPE_FILE,
- docs(03-03): update state and roadmap after plan 03-03
CI-06 stays open: it is met only when the full suite is measured under 5 minutes (later plans). - docs(03-03): complete Lua sampling profiler plan
- 3-03-02 tests: npm run profile -- <area> through Vitest
Adds dumpProfile and mergeProfiles to lua-profile.mjs, a profile dump in
setup.mjs's afterAll (only when LUA_PROFILE is an absolute folder; the
afterEach error guard is untouched), and the area mode of
scripts/profile-lua.mjs: the area must be a lowercase name of an existing
scripts/tests/<area>.test.mjs (else the valid areas are listed and the exit
is 1), Vitest runs it on one worker with sampling on, the parts are merged,
the report is printed and coverage/profile/<area>.json is written. The exit
status is Vitest's. scripts/lua-profile.test.mjs adds 8 quick tests: a hot
function ranks first, self counts add up, report sections and header,
merge sums two dumps, no dump without samples, nothing sampled with
LUA_PROFILE unset, bad areas rejected. Tests were written first; 3 failed
before the implementation.
Verified: the 8 new tests pass; npm run profile -- auction ran 74 tests green
(185757 samples); an unknown area exits 1 listing areas; smoke and errorrail
pass; npm run quality OK; npm run test:quick 195 passed. Top five inclusive
by file for auction:
179645 96.7% eval
173723 93.5% Core/Events.lua
159751 86.0% wow-stub.lua
134073 72.2% Core/Combat.lua
117581 63.3% Core/Theme.lua - 3-03-01 tests: Lua sampling profiler, npm run profile -- login
Adds scripts/lib/lua-profile.mjs (a sampler for the harness's count hook, the
accumulator, topRows and the printed report), a call to it from attachHooks
when LUA_PROFILE is set, scripts/profile-lua.mjs with the login area (one
warm-up login, then N timed logins, prints the four tables and writes
coverage/profile/login.json) and the profile npm script.
Verified: npm run profile -- login prints 4650 samples over 3 warm logins;
smoke and errorrail pass with LUA_PROFILE unset; npm run quality and
npm run test:quick pass (187). Top five self by file:
2829 60.8% wow-stub.lua
804 17.3% Core/Theme.lua
243 5.2% wow-layout.lua
144 3.1% Modules/ActionBars.lua
135 2.9% Core/DB.lua - docs(03-02): update state and roadmap after plan 03-02
- docs(03-02): complete full suite on ready-for-review plan
- 3-02-02 docs: describe the ready-for-review full-suite trigger
README.md (the coverage comment in the commands block and the CI
paragraph), scripts/tests/README.md and the header comments of
full-suite.yml, lua-suite.yml and verify.yml now state the D-04 rule: the
full suite runs when a pull request is opened ready or marked ready for
review (drafts do not run it), on every later push only while the
full-tests label is on, weekly (shuffled), from the Actions tab and
before each release; the quick check runs on every push of a non-draft
pull request. README.md and the full-suite.yml header also say that a
later push without the label stops a full-suite run still in progress for
that pull request, and that adding any other label neither starts nor
stops a run. Comments and prose only; no YAML keys changed. The "about
five minutes" sentence in lua-suite.yml is left for plan 03-07.
Verified: npm test -- scripts/workflows.test.mjs scripts/pkgmeta.test.mjs
33 passed; grep finds "ready for review" in README.md, scripts/tests/README.md
and full-suite.yml; npm run quality OK; npm run test:quick 187 passed. - 3-02-01 ci: full suite on ready for review, later pushes only with full-tests
full-suite.yml now starts the full suite when a pull request is opened
ready, marked ready for review or reopened, and on later pushes (and a
labeled event) only while the full-tests label is on the pull request.
Drafts never run it. The suite job's if also skips a labeled event for any
other label, and the concurrency group gets a "-other-label" suffix for
that case, so an unrelated label neither starts a second run nor cancels a
good one. permissions, schedule, preview, shuffle and the report job are
unchanged.
opened is added beyond D-04's literal wording: a pull request opened
ready fires opened, not ready_for_review (research pitfall 2).
scripts/workflows.test.mjs gains a small evaluator for the expression
subset those two places use (anything else throws), and a truth table
that evaluates the real if and concurrency text for each event, plus text
checks: types line, unchanged schedule/preview/report/cancel lines,
verify.yml unchanged and label-free, no VG_LOGIN_SCOPE in any workflow,
no event or input expression in any run script, no privileged pull
request event, permissions unchanged.
Verified: the 8 new trigger cases failed against the old file, then
npm test -- scripts/workflows.test.mjs 31 passed; npm run quality OK;
npm run test:quick 187 passed. Hosted behaviour (a run actually appearing,
the cancel behaviour of a skipped run, A2) is unverified until billing
clears (D-01). - docs(03-01): update state and roadmap after plan 03-01
- docs(03-01): complete suite timing recorder plan
- 3-01-03 tests: timing --baseline checks every phase-start case
- baselineDiff(lines, cases) in scripts/lib/suite-timing.mjs: multiset
compare of file|case lines with a run; reports missing, not passed (failed
or skipped) and new cases by file; a duplicate line needs one passing case
per copy. - npm run timing --baseline <file>: prints 'baseline: N of M cases present and
passed', the missing and not-passed lines and the new cases by file; exits 1
on any missing or not-passed baseline case or any skipped/todo case in the run. - scripts/suite-timing.test.mjs: 14 quick-project tests of the report and
baseline code on canned JSON (tests written first; the 5 baselineDiff
tests failed before the implementation).
Verified: npm test -- scripts/suite-timing.test.mjs (14 passed); a partial run
of smoke.test.mjs against the baseline prints 'baseline: 1 of 907 cases present
and passed' and exits 1; npm run quality and npm run test:quick pass (163 tests).
- baselineDiff(lines, cases) in scripts/lib/suite-timing.mjs: multiset
- 3-01-02 tests: record the 907 phase-start cases
Wrote 03-BASELINE-CASES.txt with npm run timing --cases-out: one sorted
file|case line per case, 907 lines. Taken at commit ac0f4e0e76ad5f88ad309c3312183c8ba7237542 (no test file differs
from the phase start). The run's tests line: tests 907 passed, 0 failed, 0 skipped
(69 files, 4 workers; names only, not a timing claim). Not edited by hand. - 3-01-01 tests: npm run timing records per-file times on 2 workers
- scripts/lib/suite-timing.mjs: pure helpers that read a Vitest JSON report
(file rows, wall span, max concurrent files, case keys, case list, machine
busy sample, printed report). - scripts/suite-timing.mjs: the npm run timing CLI; runs Vitest on a worker
cap (default 2) with the JSON reporter, prints the header, totals and the
per-file table, exits 1 on a failed test, a Vitest error, wall time over
--budget, or more files at once than the cap. --cases-out writes the sorted
case list when every case passed; --baseline is rejected until task 3. - package.json: timing script after coverage.
Verified: npm run timing on scripts/workflows.test.mjs prints max concurrent
files 1 and within budget; --budget 0.001 prints over budget and exits 1;
npm run quality and npm run test:quick pass (149 tests).
- scripts/lib/suite-timing.mjs: pure helpers that read a Vitest JSON report
- docs(phase-3): create phase plan
- docs(state): record phase 3 research session
- docs(phase-3): record post-research decisions
- docs(phase-3): research
- Merge pull request #241 from clifftaylor2325/work/design-research
- docs(state): record phase 3 context session
- docs(03): capture phase context
- docs design: add UX, one-stop feature and visual polish research
Adds three research docs under docs/design/:- UX-LESSONS.md: principles for the layout editor, profiles and
overrides, working within the 12.x restrictions, releases, onboarding,
and where to compete, with ten ranked recommendations. - ONE-STOP-FEATURES.md: 17 ranked features for WoW Forever (numbers match
issues #206-#222), what the game already covers, blocked features with
clean alternatives, and in-game tests for open questions. - VISUAL-POLISH.md: a crispness checklist for the WoW client, a palette
critique with recomputed WCAG/APCA figures, theme variants, art, type and
motion, and 15 ranked changes (numbers match issues #225-#239).
Written as principles in Vanguard's own terms; product-specific sources
are not included. Docs only; docs/ is in the .pkgmeta ignore list.
Nothing checked in game.
- UX-LESSONS.md: principles for the layout editor, profiles and
- docs: describe features in our own terms, not by other addons' names
Rewords comments and docs that compared Vanguard with other addons or
named them as inspiration ("VuhDo style", "as AdiBags and Baganator do",
"the way Titan Panel shows them", the auction plan's side-by-side, the
rename note's link to the other ForeverUI, the plan tables' "comes from"
columns) so they describe Vanguard's features in its own words. Renames
the chat watch test's fake sender from Healbot to Lightmend.
Kept on purpose: names Vanguard integrates with by code (Masque, Questie,
TomTom's /way, the DBM/BigWigs docks, LibDataBroker, Auctionator's API
names and list format), licence credits (Ace3) and download sites.
Comments, docs and test data only; no behaviour change. Not run here (the
npm registry is blocked in this environment); CI runs the quick gate. - docs(phase-2): complete phase execution
- docs(phase-2): verify phase goal
- docs(phase-2): code review and disposition
- docs(phase-2): add/update security threat verification
- docs(phase-2): add/update validation strategy
- docs(02-13): complete quality rule, stub debugstack and phase gate plan
- 2-13-03 tests: the guard's failure message shows the stack of a caught error
New case in scripts/tests/errorrail.test.mjs: a hook wrapped in ns.Guard raises
error("TEST-ONLY stack"); caught(addon) holds the entry with a stack (from the test
client's debugstack, added in 2-13-02) and the message of check([addon], []) has the
text followed by 'stack:' and its frames (D-02). The entry is named with an anchored
expectErrors pattern whose only variable part is the eval line number (D-01). This is a
new assertion in its own commit after the stub change (Phase 1 policy); nothing loosened.
Verified: npm test -- scripts/tests/errorrail.test.mjs (7 passed). - 2-13-02 harness: the test client gets a debugstack, as the 12.1 client has
scripts/lib/wow-world.lua defines debugstack(start) next to hooksecurefunc: the Lua call
stack as text, start levels above its caller (1 = the function that called it), built from
debug.traceback without the 'stack traceback:' header. Core/Events.lua's stack() already
uses debugstack when the client has it, so an entry the addon keeps in tests now carries
the failing frames and the error guard's failure message prints them (D-02).
Client source (read only): Gethe/wow-ui-source, branch forever (head 9437644),
Interface/AddOns/Blizzard_ScriptErrors/Blizzard_ScriptErrors.lua lines 53-63 call
debugstack(debugStackLevel) and use its text as the error popup's stack; Blizzard_CooldownViewer
also calls debugstack(). Whether debugstack exists on the Forever game client itself is not
verified in game (research A3): needs an in-game check.
No harness rule is loosened and no assertion changed; the one case that assumed no stack
was made explicit in the previous commit.
Verified: npm test (69 files, 906 tests, green), npm run quality, npm run lint (0 warnings). - 2-13-02 tests: the no-stack event case says it has no debugstack
The case 'events: on a client without debugstack or an error handler' relied on the
test client having no debugstack and expects an entry with stack "". It now sets
debugstack = nil in its own fixture, as the 'without debugstack' case beside it
already does, so it states the condition it is about. No assertion changed.
This lands before the stub's debugstack (next commit); it passes with and without it.
Verified: npm test -- scripts/tests/core.test.mjs (50 passed). A dry run of the whole
suite with the stub change showed this was the only case that assumed no stack. - 2-13-01 quality: hooks, clicks and timers in addon Lua must run under ns.Guard
- scripts/lib/code-quality.mjs: rule 4 rejects a hooksecurefunc, SetScript("OnClick", ...)
or C_Timer.After/NewTimer/NewTicker call in Core/ or Modules/ whose callback is not an
ns.Guard(...) call. Token-based, so a callback that starts on the next line is read; a
literal nil OnClick passes; the only exception is ns.Soon's timer in Core/Combat.lua
(a C_Timer.After whose callback calls runSoon, run under ns.Try). - scripts/code-quality.test.mjs: nine self-tests (bare/guarded, next line, commas in a
callback body, type check, OnClick/nil/OnEnter, three timer forms, the Soon exception
and its limits, comments/strings/other folders). - scripts/check-code-quality.mjs: header and OK line mention the rule (prefix kept).
- README.md: the /vg errors line now names hooks on game functions, button clicks and
timers. This is the README sentence for the behaviour plans 02-01 and 02-08 to 02-12
changed; this commit makes it hold everywhere, enforced.
Verified: npm test -- scripts/code-quality.test.mjs (35 passed); npm run quality exits 0 on
the whole repo (no bare hook, click or timer left); stripping ns.Guard( from three real
files makes the rule report 19 bare callbacks; npm run test:quick green.
- scripts/lib/code-quality.mjs: rule 4 rejects a hooksecurefunc, SetScript("OnClick", ...)
- docs(02-12): complete last hook, click and timer conversions plan
- 2-12-03 modules: minimap buttons, raid tools, talent planner, fight history and raid status run under ns.Guard
Converted sites (only the callback argument changed):- MinimapButtons.lua: click minimap button drawer, timer minimap button rescan
- RaidTools.lua: click raid tools button (insecure DialogButton), click raid target icon
(insecure iconButton with no template); the secure world-marker buttons get no OnClick - TalentPlanner.lua: click talent plan; its after() timer calls (already under ns.Try) untouched
- FightHistory.lua: timer fight history record
- RaidStatus.lua: timer ready check linger
Completeness: no unguarded hooksecurefunc( or SetScript("OnClick" in Core/ or Modules/;
the only unguarded C_Timer.After( line is ns.Soon's in Core/Combat.lua.
Verified: talents, fights, status, raid and fallbacks tests (5 files, 37 tests) pass;
full npm test 69 files / 897 tests pass; npm run coverage exit 0; quality, lint and
luac5.1 -p pass. No test file edited.
- 2-12-02 modules: tracker, broker, click-cast options, instance prep and minimap clicks run under ns.Guard
Converted sites (only the callback argument changed; all on insecure buttons):- QuestTrackerView.lua: click quest tracker collapse, click quest tracker title
(the secure quest item button gets no OnClick of ours) - BrokerStrip.lua: click broker feed
- ClickCastOptions.lua: click spell picker item, click spell picker ranks,
click close spell picker, click click-cast cell, click click-cast test tile - InstancePrep.lua: click close instance prep
- Minimap.lua: click minimap world map
Verified: broker, clickcast, prep, frames, auctionrestock and more tests (6 files,
140 tests) pass with the guard on; test:quick, quality, lint and luac5.1 -p pass;
no unguarded site is left in these five files. No test file edited.
- QuestTrackerView.lua: click quest tracker collapse, click quest tracker title
- 2-12-01 quests: nameplate, swing timer and quest view hooks and clicks run under ns.Guard
Converted sites (only the callback argument changed):- Nameplates.lua: hook UpdateHitTestArea
- SwingTimer.lua: hook ApplyRangePresentation
- QuestLog.lua: click quest log toggle, hook ToggleQuestLog
- QuestLogView.lua: click close quest log, click quest log button, click quest log line
- QuestDialogView.lua: click quest dialog primary, click quest dialog secondary, click quest dialog line, click quest reward choice
Verified: plates, units, windows-map and core tests (4 files, 104 tests) pass with the
guard on; test:quick, quality, lint and luac5.1 -p pass; no unguarded site is left in
these five files. No test file edited.
- docs(02-11): complete window-skin, menu strip, game tool, alert, tooltip, chat and bar conversions plan
- 2-11-03 chat: chat, chat extras, chat watch, cast bar and data bar hooks and clicks run under ns.Guard
Converted 9 sites (only the callback argument changed):- Chat.lua: after() wrapper as hook <name> (covers its four callers)
- ChatExtras.lua: hook AddMessage (02-05 hooksecurefunc type check kept), hook FCF_OpenTemporaryWindow, click copy chat; HookScript OnMouseWheel untouched
- ChatWatch.lua: click chat watch toast
- CastBars.lua: hook UpdateBarFillTexture, hook PlayFadeAnim
- DataBars.lua: hook Show, hook SetShown (f.Hide passed through the guard, called with the frame as before)
AddMessage, Show and SetShown are hot; noted for the in-game perf list.
Verified: 6 area files (170 tests) pass, quality, lint and luac5.1 -p pass, full npm test 69 files / 897 tests pass. No test file edited.
- 2-11-02 windows: town window, menu strip, game tool, alert and tooltip hooks run under ns.Guard
Converted 10 hook sites (only the callback argument changed):- WindowsTown.lua: hook UpdateItems, hook MasterLooterFrame_UpdatePlayers
- MenuStrip.lua: hook Layout, hook SetCollapsed, hook Update
- GameTools.lua: hook RefreshLayout, hook Update, hook SetupSessionWindow
- GameAlerts.lua: hook AddMessage (fires per line of that frame; on the in-game perf list)
- Tooltips.lua: hook GameTooltip_SetDefaultAnchor
Verified: 5 area files (154 tests) pass with the guard on, test:quick, quality, lint and luac5.1 -p pass. No test file edited.
- 2-11-01 windows: window-skin hooks run under ns.Guard
Converted 13 hook sites (only the callback argument changed):- Windows.lua: hook SetupTimer, hook InitButtons, and both calls inside after() as hook <name> (built per hook installed)
- WindowsMap.lua: hook SynchronizeDisplayState, hook Refresh, hook Update
- WindowsSocial.lua: after() inline closure as hook <key>
- WindowsSystem.lua: hook OnSelected, hook UpdateExtraButtons, hook Blizzard_CombatLog_QuickButton_OnClick
- WindowsTabs.lua: hook AddTab, hook EquipmentFlyout_UpdateItems
Verified: 8 window/spellbook area files (101 tests) pass with the guard on, test:quick, quality, lint and luac5.1 -p pass. No test file edited.
- docs(02-10): complete remaining Core hook, click and timer conversions plan
- 2-10-03 core: options, visibility and perf clicks and timers run under ns.Guard
Eight callbacks now report a failure to ns.errors and /vg errors. Only the
callback argument changed at each site.- Core/OptionsProfiles.lua (1): click danger button (18, W.Danger's Sure? click)
- Core/OptionsWindow.lua (3): click reset setting (177), click close options
(432), click options page (484); the GetScript("OnClick")(...) call at 581
still works because the wrapper passes arguments through - Core/Visibility.lua (3): click visibility chip (288), timer end linger (115,
V.EndLinger), timer auto hide (134, T.FadeOut) - Core/Perf.lua (1): timer perf report (133, the C_Timer.NewTicker)
Verified: options, visibility, bars, profiles and core tests (5 files / 133
tests) pass with the guard on; quality, lint and luac5.1 -p pass; no unguarded
site is left in the four files. Full npm test: 69 files / 897 tests passed on
the first run; npm run coverage: exit 0, Lua line coverage 19058/19058 100.00%.
No test file was edited.
- 2-10-02 core: dialog, error badge, feedback, game menu and setup clicks run under ns.Guard
Twelve OnClick handlers now report a failure to ns.errors and /vg errors.
Only the callback argument changed at each site.- Core/Dialog.lua (6): click close text box (64), click text box primary (87),
click question yes (116), click question no (117), click reload by hand
(158, the plain button's fallback only; the SecureActionButtonTemplate
overlay is untouched), click reload later (172) - Core/ErrorBadge.lua (1): click list errors (54)
- Core/Feedback.lua (1): click cancel feedback (267)
- Core/GameMenu.lua (2): click open options from game menu (34, 43)
- Core/Setup.lua (2): click setup style (381), click close setup (455)
Verified: setup, feedback, profiles, options and core tests (5 files / 115
tests) pass with the guard on; npm run test:quick, quality, lint and luac5.1 -p
pass; no unguarded site is left in the five files. No test file was edited.
The Reload button's secure overlay needs an in-game check.
- Core/Dialog.lua (6): click close text box (64), click text box primary (87),
- 2-10-01 core: mover, profile and theme hooks run under ns.Guard
Six hooksecurefunc callbacks now report a failure to ns.errors and /vg errors
instead of escaping. Only the callback argument changed at each site.- Core/Movers.lua: hook SetAlpha (inline), hook SetPoint (inline, the re-pin),
hook HighlightSystem (hideHandle) - Core/Profiles.lua: hook UpdateLayoutInfo (onLayouts)
- Core/Theme.lua: hook SetChecked (onTabChecked), hook SetTabSelected (repaintTab)
SetPoint, SetAlpha and SetChecked fire often; they are on the in-game perf
list (research A4): measure with /vg perf.
Verified: movers, unlock, theme, profiles, options and windows-tabs tests
(6 files / 88 tests) pass with the guard on; npm run test:quick, quality, lint
and luac5.1 -p pass; no unguarded site is left in the three files. No test
file was edited.
- Core/Movers.lua: hook SetAlpha (inline), hook SetPoint (inline, the re-pin),
- Merge plan 02-09: bags, vendor and inventory conversions
- Merge plan 02-08: auction and widget factory conversions
- docs(02-08): summary for the money-path conversions
- 2-08-02 widgets: the widget and control factories wrap every OnClick they install
Only the callback argument changed at each site. A nil onClick stays nil
(ns.Guard returns non-functions unchanged). New W.ClickLabel(text) in
Core/Widgets.lua returns "click <caption>" for a string caption, else
"click button"; it is built once when the button is made.
Converted sites (label):- Widgets.lua W.Button "click <caption>" (W.ClickLabel)
- Widgets.lua W.Cycle "click cycle"
- Widgets.lua W.Swatch "click swatch"
- Widgets.lua W.KeyCapture "click key capture"
- Controls.lua W.Primary "click <caption>" (W.ClickLabel)
- Controls.lua W.Check "click switch"
- Controls.lua segment "click segment"
- Controls.lua W.Choice "click choice"
- Controls.lua menu item "click menu item"
- Controls.lua W.Dropdown "click dropdown"
- Controls.lua W.Slider "click slider"
This covers the auction Post, Max, Stop, Skip, Relist, Cancel, Buy and Back
buttons (all built with W.Button / W.Primary).
Verified: options, setup, clickcast, profiles and auction tests pass with the
guard on; full npm test 69 files / 897 tests passed; npm run coverage exit 0
with Lua line coverage 19058/19058 (100.00%); quality, lint and luac5.1 -p
pass. No test file changed. Needs an in-game check: auction Post, Buy and
Cancel after wrapping.
- docs(02-09): complete money-path conversions, bags, vendor and inventory plan
- 2-09-02 vendor: errands, merchant and inventory clicks, hooks and timers run under ns.Guard
Only the callback argument changed at each site. Converted sites (label):- Modules/Errands.lua: timer errands (inside later(), around fn before
C_Timer.After; the no-timer fallback that calls fn directly is as it
was), click train all (ER.TrainAll, still inside its click), hook
TakeTaxiNode (ER.OnTakeTaxi) - Modules/Merchant.lua: timer sell junk (MC.SellJunk retry, inside its
existing C_Timer check) - Modules/Inventory.lua: timer bag scan (IV.Scan)
Purchase, sell and training calls run where they ran: the timer callback
stays a timer callback, the click stays in its click.
Verified: vendor, inventory, auctioncrafting and auctionrestock tests
pass (4 files / 24 tests); full npm test passes with the guard on
(69 files / 897 tests); quality, lint and luac5.1 -p pass. Not observed
in game (train all, restock buying, junk selling need an in-game check).
- Modules/Errands.lua: timer errands (inside later(), around fn before
- 2-08-01 auction: hooks, clicks and timers run under ns.Guard
Only the callback argument changed at each site; every call stays in the
click's or hook's own call stack. Converted sites (label):- AuctionCrafting.lua: hooksecurefunc(form, "Init") "hook Init"
- AuctionHelper.lua: AH.Row OnClick "click auction row"
- AuctionHelper.lua: sendLater timer "timer search queue"
- AuctionHelper.lua: AH.Next fallback timer "timer search fallback"
- AuctionHelper.lua: hook SetDisplayMode "hook SetDisplayMode"
- AuctionHelper.lua: hook UpdateTitle "hook UpdateTitle"
- AuctionSell.lua: sell slot OnClick (AH.TakeCursor) "click sell slot"
- AuctionTable.lua: sort header OnClick "click sort header"
- AuctionOwned.lua: paintLater timer "timer repaint owned"
The SetPostItem hook in AuctionSell.lua was already guarded (02-01).
Verified: auction, auctionapi, auctioncrafting, auctionledger, auctionrestock,
auctionupgrades and errorrail tests pass with the guard on (7 files, 115
tests); test:quick, quality, lint and luac5.1 -p pass. No test file changed.
Needs an in-game check: auction Post, Buy and Cancel after wrapping.
- 2-09-01 bags: bag, bag-section and upgrade-marker hooks run under ns.Guard
Only the callback argument changed at each site; the type checks around
them are as before. Converted sites (label):- Modules/Bags.lua: hook SetVertexColor, hook Hide (per-slot quality
border), hook GenerateItemSlotsForSelectedTab, hook Show (game.Hide),
hook UpdateSearchBox, click sort bags (sort button OnClick),
hook UpdateItems - Modules/BagSections.lua: hook UpdateItemLayout, hook UpdateItems,
hook UpdateCurrencyFrames - Modules/Upgrades.lua: hook UpdateItems (bag upgrade marker)
Perf note: Bags.lua SetVertexColor and Hide hook every bag slot's border
and fire often; the guard adds one wrapper call and an xpcall per firing
(research A4). On the in-game perf list (/vg perf with bags open).
Verified: bagsections, upgrades, auctionupgrades, inventory, windows-town
and visibility tests pass with the guard on (6 files / 52 tests);
test:quick, quality, lint and luac5.1 -p pass. Not observed in game.
- Modules/Bags.lua: hook SetVertexColor, hook Hide (per-slot quality
- docs(02-07): summary for switching the error guard on
Records the full-suite and coverage results (100.00%), the phase D-04 tally (5 root causes) and the negative control on the SetPostItem hook. - 2-07-01 harness: the error guard judges every test, with no opt-in
- scripts/tests/setup.mjs: the global afterEach always calls check(createdAddons.splice(0), takeExpected()); the VG_ERROR_GUARD condition is gone, the only process.env read left is LUA_COVERAGE_HITS, and no test file is named.
- scripts/tests/README.md: test writers are told every test is judged, how to name a deliberate error with expectErrors (exact text, or an anchored pattern varying only in digit runs), that a broader matcher throws and an unused marker fails, and that AURA_FALLBACK is the D-06 notice.
- Phase D-04 total so far: 5 distinct unexpected root causes (R1 Enum replaced by fixtures, R2 hooksecurefunc fixture string form, R3 ChatExtras without hooksecurefunc/RegisterCallback, R4 AFK bare client, R5 PowerType read); the other guarded entries were deliberate and carry markers. Under the 20 limit; no step-1 failure, so no extra file.
Verified: VG_ERROR_GUARD=1 npm test (69 files / 897 tests) green before the switch; npm test green after (69 / 897); npm run quality OK; npm run coverage 19055/19055 (100.00%).
- Merge plan 02-06: wave 2 markers and fixes
- Merge plan 02-05: wave 2 markers and fixes
- Merge plan 02-04: wave 2 markers and fixes
- Merge plan 02-03: wave 2 markers and fixes
- docs(02-05): summary for the chat, tooltip, frames and more error fixes
- 2-05-03 tests: the AFK bare client keeps its other modules off, and the deliberate entries are named
- R4: the errors in the AFK case did not come from UNITS_ENV. The 'a client that
can't say much' part strips UnitName, UnitLevel, UnitClass and UnitXP from a
client with no UNITS_ENV, so UnitFrames, ComboPoints, ClassBars, SwingTimer,
InfoStrip and Inventory (which call UnitClass or UnitName when they enable)
failed to enable. The card is the point of that part, so those six modules are
switched off in that client's saved variables. UNITS_ENV is unchanged. - Markers (expectErrors, each fully anchored, only the line number varies):
more.test.mjs 'game menu button: falls back...':
/^GameMenu: wow-world.lua:\d+: CreateFrame: unknown template NoSuchTemplate$/
more.test.mjs 'game windows: one skin that fails is reported...':
/^window CharacterFrame: eval:\d+: TEST-ONLY skin failure$/
tooltip.test.mjs 'item tooltips: other modules' lines come after...':
/^tooltip lines: eval:\d+: TEST-ONLY broken line$/ - The wow-world.lua line is a pattern too: that file belongs to another plan this
wave and its line numbers move. - Verified: VG_ERROR_GUARD=1 on more, tooltip, frames, profiles, windows-map and
windows-menus passes (156 tests); npm test passes without the opt-in (69 files,
897 tests); npm run quality OK.
- R4: the errors in the AFK case did not come from UNITS_ENV. The 'a client that
- 2-05-02 tests: Enum and hooksecurefunc fixtures match the client
- R1: tooltip, frames and the three damage-meter cases in more.test.mjs replaced
Enum wholesale, so AuraEngine failed on Enum.CustomAuraButtonDispelTypeTextureStyle
('debuff type colors', 'aura icon extras'). They now extend Enum. The case that
removed Enum on purpose now removes only the two damage-meter enums, so the
'no client enums' assertion is unchanged and other modules keep their Enum. - R2: the boss-frame hooksecurefunc fixture accepts the (name, fn) form as the
stub does and leaves a missing function alone, ending 'attempt to index a
string value (local t)' in Chat, ChatExtras and Tooltips. - No markers and no assertions changed.
- Verified: tooltip, frames, more pass unguarded; frames passes under
VG_ERROR_GUARD=1; the guarded tooltip+more log holds none of the R1/R2 texts,
and fails only on the AFK case and the deliberate cases for task 3.
- R1: tooltip, frames and the three damage-meter cases in more.test.mjs replaced
- docs(02-03): summary for core.test.mjs under the error guard
- 2-03-02 tests: core error-handling cases name the errors they cause
Each case below tests the error rail, a failing module, upgrade step, pin, handler or event, or a client without cooldown frames or the aura container, so it names every entry with expectErrors (D-01). Exact strings unless noted.- account version upgraded: DB upgrade 4: eval:2: boom
- failing first upgrade step: DB upgrade 1: eval:2: boom
- modules: module Bad: eval:4: boom
- module whose settings fail to upgrade: /^module DataBars: Modules/DataBars.lua:\d+: attempt to compare number with string$/ (file line varies), module TESTONLY_Broken: eval:3: attempt to compare number with string
- pins throwing apply: /^pin Vanguard_PinTest: eval:\d+: boom$/ (fixture line varies), pin frame: eval:2: bare
- capability probe what this client has: aura icon / module PlayerAuras / module ActionBars: eval:3: TEST-ONLY: no cooldown frames (the fixture throws it)
- capability probe every flag off: AURA_FALLBACK (D-06)
- secure snippets waiter: secure snippet fallback: eval:3: TEST-ONLY waiter broke
- /vg errors what Vanguard caught: /^TEST-ONLY: \d+$/ (the 20-entry cap case, counter varies)
- caught errors one entry per distinct error: TEST-ONLY site: boom, TEST-ONLY try: eval:2: bad x, TEST-ONLY inner: in, TEST-ONLY outer: eval:2: out
- stack read while frames are live: TEST-ONLY live / event TEST_ONLY_LIVE (eval:2: TEST-ONLY deep), TEST-ONLY late: x
- without debugstack: TEST-ONLY bare: x, TEST-ONLY: (a hidden error message)
- game handler loop: TEST-ONLY outer: x, TEST-ONLY inner: TEST-ONLY outer: x, TEST-ONLY none: y
- after-combat update: after-combat update: eval:2: TEST-ONLY late
- /vg errors counts and clear: TEST-ONLY twice: x, TEST-ONLY once: y
- four events cases (throws, no debugstack, broken handler, CallErrorHandler): event TEST_ONLY_EVENT: eval:5: TEST-ONLY first
- game handler fails inside failing handler: the same event text, TEST-ONLY again: second
- unit events: event UNIT_LEVEL: eval:2: TEST-ONLY unit handler, event TEST_ONLY_GONE: not on this client
- error badge: TEST-ONLY site: TEST-ONLY failure, TEST-ONLY site: TEST-ONLY other
Verified: VG_ERROR_GUARD=1 and plain npm test scripts/tests/core.test.mjs both 50/50; npm run quality OK.
- docs(02-04): summary for the aura, nameplate and report error markers
- 2-04-02 tests: name the entries the /vg report cases plant
Marked cases (expectErrors), verified against the guarded run:- 'lists the caught errors': exact 'RaidFrames: attempt to index a nil value' and 'Nameplates: |cffff0000bad|r arithmetic' (colour escapes kept)
- 'stays under 8,000 characters': one pattern GENERATED = new RegExp('^Module\d+: ' + 'long message '.repeat(30) + '\d+$'), anchored at both ends, varying only in two digit runs. The 20-entry cap drops the oldest entries; the case plants exactly 20 and asserts 20 remain, so the guard judges all 20 that are kept.
- 'names the modules turned off and the skinned addons': '^skin for Broken: eval:\d+: oops$' (the Broken skin throws on purpose; only the line number varies). Not in research's list; confirmed deliberate because the case asserts 'Broken (failed)'.
Verified: VG_ERROR_GUARD=1 and plain npm test on report.test.mjs (5 pass each), npm run quality OK. No unexpected entries, no addon change.
- 2-03-01 tests: core fixtures extend Enum and hook both hooksecurefunc forms
- The capability-probe fixtures (CLIENT_FULL_ENV and the three half cases) no longer replace the stub's Enum; they set or clear only the four tables the probe asks about (R1 debuff type colors / CustomAuraButtonDispelTypeTextureStyle, R5 UnitPowerMax / Enum.PowerType).
- The test-local hooksecurefunc in PIN_ENV accepts (name, fn) as well as (table, name, fn), as the client and the stub do (R2: Chat, ChatExtras, Tooltips failed to enable).
- No marker added; the stub is unchanged. Verified: npm test core.test.mjs passes (50/50); the guarded run no longer lists the R1, R2 or R5 texts; npm run quality OK.
- 2-04-01 tests: name the aura and nameplate fallback notices, D-06 included
Marked cases (expectErrors), each verified against the guarded run:- auras: 'a client that won't drive the dispel highlight' -> two patterns anchored at both ends, varying only in the Core/AuraEngine.lua line number: 'debuff type colors: ...' and 'aura icon extras: ...' (index of nil Enum.CustomAuraButtonDispelTypeTextureStyle, wrapped by ns.Try on purpose)
- auras: AURA_FALLBACK (D-06) for the raid-on-fallback, container-fallback, UNIT_AURA-skip, enchant-switch, filter-rejection and stripped-filter cases
- auras: 'event WEAPON_ENCHANT_CHANGED: not on this client' (enchant switch case)
- auras: exact 'aura filter ...' texts for the rejected-word, reject-everything and stripped-filter cases
- auras: probe case 'the client's AuraContainer has no AddAuraGroup' (exact text)
- auras: three refusal cases, patterns anchored at both ends varying only in the eval:<n> digits (SetUnit refused, no more containers, HoT row refused)
- plates: AURA_FALLBACK on the first-drawn-in-combat case (noAuraContainer client)
Verified: VG_ERROR_GUARD=1 and plain npm test on auras and plates (36 pass each), npm run quality OK. No unexpected entries, so no addon change and nothing for the D-04 tally.
- 2-05-01 chat: ChatExtras skips its hook and link watcher on a client without them
- watchWindow hooks AddMessage only when hooksecurefunc is a function (the other
36 sites already check); watchLinks returns early when the registry has no
RegisterCallback. Root cause R3: ChatExtras failed to enable on those clients and
left 'module ChatExtras: ...' in ns.errors (profiles, windows-map, windows-menus). - Both changes extend an existing condition; no line added, behaviour with both
calls present is unchanged. Nothing a player sees changes; no README edit needed. - Verified: VG_ERROR_GUARD=1 profiles, windows-map, windows-menus pass; more and
chatwatch pass unguarded; lint, luac -p and quality exit 0.
- watchWindow hooks AddMessage only when hooksecurefunc is a function (the other
- docs(02-06): complete deliberate-error markers plan
- 2-06-02 tests: name the deliberate errors in the fallback and skin tests
Each case now names its one expected ns.errors entry with expectErrors (exact text, no patterns):- fallbacks.test.mjs, 'without issecretvalue nothing is secret, and a failing after-combat update is reported...': "after-combat update: eval:2: TEST-ONLY boom"
- skins.test.mjs, 'skin API': "skin for Broken: eval:5: oops"
Verified: both files pass with and without VG_ERROR_GUARD=1 (6 tests); npm run quality OK. No other entry appeared, so nothing was fixed in the addon.
- 2-06-01 tests: name the deliberate errors in the engine, auction API and feedback tests
Each case now names its one expected ns.errors entry with expectErrors (exact text, no patterns):- lua-engine.test.mjs, 'a Lua error carries a traceback naming the addon file': "TEST-ONLY: eval:2: attempt to index a nil value (local 't')"
- auctionapi.test.mjs, 'RegisterForDBUpdate: callbacks run once...': "Vanguard.API.v1 update for TEST-ONLY Broken: eval:3: TEST-ONLY broken"
- feedback.test.mjs, 'a bug's block carries the kind, title...': "RaidFrames: attempt to index a nil value"
Verified: the three files pass with and without VG_ERROR_GUARD=1 (28 tests); npm run quality OK. No other entry appeared, so nothing was fixed in the addon.
- Merge plan 02-01: ns.Guard tracer and opt-in error guard
- docs(02-01): summary of the error guard tracer plan
- 2-01-02 harness: unit tests for the guard; clicks, timers, harvest and the aura notice proved
- scripts/error-guard.test.mjs (quick project): judge, check, expectErrors and takeExpected on hand-built entries, including every refused and accepted matcher (D-01).
- scripts/tests/errorrail.test.mjs: a throwing OnClick and a throwing C_Timer callback under ns.Guard, non-function and no-return behaviour, the ns.ClearErrors harvest, the D-06 aura notice.
- scripts/tests/helpers.mjs: re-exports expectErrors; AURA_FALLBACK, the D-06 notice matcher. The probed notice text matched the plan's pattern, no difference.
Verified: new files pass with and without VG_ERROR_GUARD=1; the guarded feedback.test.mjs run fails on 'RaidFrames: attempt to index a nil value (x1)' (negative control, marker comes in plan 02-06); npm test 68 files / 890 tests green; quality and lint clean.
- docs(02-02): complete shuffle config, link-wow order fix and weekly shuffled run plan
- 2-02-03 ci: The Sunday run is shuffled and prints its seed; other runs are not
- lua-suite.yml: new boolean shuffle input (default false) and a step, before npm run coverage and only when inputs.shuffle, that takes the seed from date +%s, appends VG_SHUFFLE_SEED to GITHUB_ENV and prints the seed with the bash and PowerShell replay lines to the log and the job summary. No workflow expression sits in any run text; permissions unchanged (contents: read).
- full-suite.yml: passes shuffle: github.event_name == 'schedule', so only the weekly run shuffles (D-05). release.yml is unchanged and keeps the default off.
- scripts/workflows.test.mjs (quick project): reads the three files as text and checks the input, the seed step and its order, the schedule-only switch, release without shuffle, no input expression in any run text, and read-only permissions.
- Verified locally: workflows test 7 passed; npm run test:quick 9 files, 129 tests passed; npm run quality OK.
- Not verified: the first green hosted shuffled run. The hosted runner is blocked by billing; that part of roadmap criterion 3 stays open by D-05.
- 2-02-02 ci: VG_SHUFFLE_SEED turns on a seeded shuffle that Vitest prints
- vitest.config.mjs reads VG_SHUFFLE_SEED once (positive whole number, else 0); when set, the root test options get sequence.shuffle for files and tests plus the seed, so both projects inherit it. Unset leaves the order as before.
- Header comment gives the replay in bash and PowerShell forms, what the seed does and does not fix, and that no option re-runs a failed case (D-05).
- Verified: seed 4242 quick run exits 0 and prints Running tests with seed "4242"; seeds 1, 2, 3 each exit 0 (8 files, 122 tests); unshuffled quick run exits 0 with no seed line; npm run quality exits 0.
- 2-01-01 core: ns.Guard on the SetPostItem hook, with the error guard behind it
- Core/Events.lua: ns.Guard(where, fn) wraps a hook, click or timer callback in ns.Try; a non-function comes back unchanged; returns nothing.
- Modules/AuctionSell.lua: the SetPostItem hook (Post path) runs under ns.Guard.
- scripts/lib/lua-harness.mjs: createAddon registers every addon (createdAddons, on globalThis) and keeps the entries ns.ClearErrors drops.
- scripts/lib/error-guard.mjs: expectErrors (D-01 matcher rule), takeExpected, caught, judge, check.
- scripts/tests/setup.mjs: global afterEach drains the registry; it judges only when VG_ERROR_GUARD=1 (staging switch, removed by plan 02-07).
- scripts/tests/errorrail.test.mjs: a throwing SetPostItem hook is reported, the game's SetPostItem still runs, the guard names the entry.
Verified: errorrail passes with and without VG_ERROR_GUARD=1 (smoke too), auction.test.mjs 74/74, test:quick green, quality and lint clean, luac5.1 -p accepts both Lua files.
The README /vg errors sentence does not change here: it is deferred to plan 02-13 (task 2-13-01), because it is true only once every hook, click and timer is converted (plans 02-08 to 02-12).
- 2-02-01 tests: link:wow cases each build their own folder, so any order passes
- Each case calls fresh() for its own TEST-ONLY home, game folder, io and slot under one temp root; small helpers (withAddons, withConfig, withLink) set up the starting state
- cli() takes the case's home; header no longer says the cases share a folder
- All 12 titles and every expect kept with the same expected values (37 before, 38 after: withLink asserts its link)
- Verified: 12 passed in written order and under shuffle seeds 1, 2, 3 and 20261010; npm run quality exits 0
- Order dependence fixed at its cause; no re-run added (D-05)
- docs(state): begin phase 2 execution
- docs(phase-2): renumber duplicate threat IDs
- docs(phase-2): create phase plan
- chore: set workflow mode to yolo
- docs(phase-2): research and validation strategy; record post-research decisions
- docs(state): record phase 2 context session
- docs(02): capture phase context
- docs(phase-1): complete phase execution
- docs(phase-1): verify phase goal
- docs(phase-1): add/update security threat verification
- docs(phase-1): add/update validation strategy
- 1-02-03 planning: record #169 outcome and close the issue
Posted the approved seven-row comment on #169 and closed it as completed
(D-05, D-07). The user's checkpoint replies are quoted in the summary. The
branch and PR decision is deferred by the user. Verified: issue reads
CLOSED COMPLETED with one comment. - 1-02-01 planning: draft the #169 outcome comment
Drafted the seven-row outcome comment for #169 (draft only, nothing posted).
Baseline numbers come from 01-01-SUMMARY.md (eb208c4, 66 files, 873 tests,
19050/19050 lines). Each file:line reference was checked by grep on
origin/main; the Blizzard citations are copied from RESEARCH, pinned to
wow-ui-source forever at 2ced784. Client points without a source (case 5
rect, case 6 plate position) are marked not verified in game. - 1-01-03 planning: record Phase 1 baseline evidence
Clean npm ci checkout of origin/main (eb208c4): five #169 files 120/120, verify 873/873 with quality OK, coverage 19050/19050 (100.00%). No tracked file changed outside .planning/ (D-06). Headless evidence only; nothing checked in game. - docs(phase-1): create phase plan
- docs(phase-1): research and validation strategy; revise D-02/D-03
- docs(state): record phase 1 context session
- docs(01): capture phase context
- docs: create roadmap (12 phases)
- docs: define v1 requirements
- docs: complete project research
- chore: add project config
- docs: initialize project
- docs: map existing codebase
- Merge pull request #205 from clifftaylor2325/work/open-issues-batch
Open issues batch: wizard, auction tabs, raid Width, spellbook - #197 windows: the spellbook gets the flat Vanguard panel
SpellBookFrame was missing from the framed windows, so it kept the game's parchment. It is registered with the other framed windows now (README already says the spellbook gets the flat panel). The paging arrows on a one-page book are not changed here.
Verified: new registration test in scripts/tests/spellbook.test.mjs (harness only; not seen in game). - #199 options: a raid Width the presets don't list shows its value
A saved width outside 72, 90, 100 and 120 left the segments with nothing highlighted. The Width row's choices are now marked with ns.Widgets.Keep: its choice list also includes the value in use, so the row shows it as a dropdown. Presets stay as segments, and no other choice row changes.
Verified: new test in scripts/tests/options.test.mjs (harness only; not seen in game). - #188 auction: the Shopping and Selling tabs are sized to their names
The game's tab template keeps a fixed width, so "Shopping" and "Selling" were cut off. The addon now sizes its two tabs with PanelTemplates_TabResize (20 px of padding, 70 px at least), as docs/design/AUCTION-TABS.md describes. The harness's test-only PanelTemplates_TabResize resizes like the game's, and its global is listed in .luacheckrc.
Verified: new test in scripts/tests/auction.test.mjs (harness only; not seen in game). - #186 #202 setup: the wizard sits above the unlock handles; its title and caption are centred
#186: the wizard was on the DIALOG strata, the same as the unlock handles, which are built later and drew over its steps. It now uses FULLSCREEN_DIALOG, the strata Vanguard's other dialogs use.
#202 (the wizard part): the title and the step caption were left-aligned. They are centred across the window now. The game windows' back arrows are not changed here.
Verified: new tests in scripts/tests/setup.test.mjs (harness only; not seen in game). - Merge pull request #185 from clifftaylor2325/claude/add-claude-md
Add CLAUDE.md for Claude Code sessions - Add CLAUDE.md for Claude Code sessions
Gives Claude the commands, enforced rules, layout and working method for
this repo so sessions start from the project's own conventions.
This mod has no additional files