VanguardUI

All-in-one UI suite for WoW Forever: unit & raid frames, click casting, action bars, bags, nameplates, quest tracker and more. 52 switchable modules, built from scratch. Free.
Back to Files

v0.6.0

File nameVanguard-v0.6.0.zip
Uploaded
Oct 14, 2026
Downloads
6
Size
1.0 MB
Flavors
RetailForever
File ID
9168867
Type
R
Release
Supported game versions
  • 12.0.7
  • 1.60.1

What's new

Vanguard

v0.6.0 (2026-10-14)

Full Changelog

  • 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.
  • 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.
  • 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 that money in
    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 read iy as a misspelling of it. 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 flagged light takes 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

  • 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

  • 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).
  • 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.
  • 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).
  • 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.
  • 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).
  • 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.
  • 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).
  • 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.

  • 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).

  • 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.
  • 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.
  • 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.
  • 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.
  • #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.
  • 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).
  • 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).
  • 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.
  • 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.
  • 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%.
  • #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.
  • #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).
  • #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.
  • 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.
  • 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).
  • #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 runs npm 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.
  • 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.
  • #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.
  • #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.
  • 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.
  • #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 passes SI.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) every seconds from 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); now runs 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).
  • 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 get movable = true and
    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 private after helper is now built on afterWith, 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.
  • #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.
  • 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 bare git config in 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.
  • 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.
  • #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 4 over 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.
  • 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.
  • 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 loggedIn keep-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 74 for auction.test.mjs, keep-full 32 of 35 for
      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.
  • 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.
  • 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).
  • 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).
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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).
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.