Skip to content

build(deps-dev): bump js-yaml from 4.3.1 to 5.4.1 - #7

Closed
dependabot[bot] wants to merge 45 commits into
mainfrom
dependabot/npm_and_yarn/js-yaml-5.4.1
Closed

dependabot[bot] wants to merge 45 commits into
mainfrom
dependabot/npm_and_yarn/js-yaml-5.4.1

Conversation

@dependabot

@dependabot dependabot Bot commented on behalf of github Sep 1, 2026

Copy link
Copy Markdown
Contributor

Bumps js-yaml from 4.3.1 to 5.4.1.

Changelog

Sourced from js-yaml's changelog.

[5.4.1] - 2026-08-26

Changed

  • Hard-limit merge sequence size to 100.

Security

  • Count empty mappings in merge sequences toward maxTotalMergeKeys to limit CPU usage, #797.

[5.4.0] - 2026-08-25

Added

  • Added the scalarStyleRules dumper option to customize string formatting. See Scalar styling for details.

Changed

  • [breaking] Flattened the low-level AST node style representation. Scalar and collection nodes now use SCALAR_STYLE and COLLECTION_STYLE values; explicit tags use the separate tagged property. Alias nodes now contain only kind and anchor. This only affects code that directly constructs or edits AST nodes.
  • [breaking] The sortKeys option was rewritten using AST mutation to avoid side effects.
  • Reworked scalar style selection. This can change formatting without changing loaded values; in particular, whitespace-only strings are now double-quoted.

Fixed

  • Accept a byte order mark at the start of each document in a stream, #791.
  • Produce valid flow mappings with quoteFlowKeys and flowSkipColonSpace, including alias and property-only keys, #786.
  • Preserve empty scalar items when converting block sequences to flow style.
  • Do not apply the 1024-character simple-key limit to flow mapping keys.
  • Count Unicode code points, rather than UTF-16 code units, for the 1024-character simple-key limit.
  • Add an explicit document-end marker after keep-chomped block scalars when needed to preserve trailing newlines.

[5.3.0] - 2026-08-14

This release focuses on reworking the documentation and making small architectural improvements before moving forward.

Added

  • Added completely new documentation.
  • Exported DUMP_SCHEMA, the default schema used by the dumper.
  • Added YAMLException.throwAt() for throwing an error at a source position.

Changed

... (truncated)

Commits

Dependabot compatibility score

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:

  • @dependabot rebase will rebase this PR
  • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
  • @dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
  • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)

VKrishna04 and others added 30 commits August 12, 2026 08:15
Five platforms ship, and most of the tree still described three. Every claim
below was checked against the code rather than against another document.

Corrected:

- CONTRIBUTING and the testing guide both told you to load the extension from
  `src/`. That has never worked: the source tree holds manifest-chromium.json
  and manifest-firefox.json, and the build is what emits one of them as
  manifest.json. Both now say dist/chromium, and say why.
- CONTRIBUTING called the GeeksForGeeks and Codeforces handlers the most-wanted
  contributions. Both shipped long ago.
- The testing guide had no NeetCode or takeuforward section at all, and asserted
  a GFG "Alpha" badge that constants.js has marked STABLE for some time. Status
  badges are now described as what they are — a projection of
  CONSTANTS.PLATFORMS.status — so the doc cannot drift from the code again.
- The backlog listed the LeetCode profile import overhaul as active with GraphQL
  400s and missing tags. That work landed; it moves to Completed alongside the
  Codeforces import and the two new handlers.
- support.html, terms.html, PRIVACY.md, the web manifest and the landing page
  all enumerated three platforms.
- The landing page announced v1.0.0 in the hero and in its SoftwareApplication
  JSON-LD while package.json read 1.7.0.

Two of these were not documentation at all:

- CanonicalView hardcoded a three-platform list, so NeetCode and takeuforward
  could not be linked to anything. Cross-platform identity is the entire purpose
  of that screen, and folding a NeetCode solve onto its LeetCode twin is the
  reason the NeetCode handler does a second slug lookup. It now derives the list
  from CONSTANTS.PLATFORMS.
- The page title claimed three platforms in a field too short to hold five.
  Naming three of five reads as exhaustive, so it names none and the meta
  description carries them.

CI has been failing on main since before this branch of work, on format:check
against seven files. They were left alone as pre-existing and not mine to
reformat, which was the wrong call: the cost of that restraint was a red
default branch. Formatted, and the local gate now matches CI.

Gate: 990/990 tests, no fatal type errors, format:check clean, build:fast ok.
Codeforces was labelled alpha for a reason that was never written down. The
audit found six concrete defects behind that label, two of which silently
corrupted the ledger.

Submitting on Codeforces posts a form and lands you on /contest/{id}/my — a
table of *every* submission in the contest. Taking the first accepted row
meant filing whichever problem you had solved earliest, under the code you
had just written, before the judge had answered the run you actually made.
A submission that ended in Wrong Answer was recorded as a solve. An accepted
row must now link back to the problem whose code was captured
(matchAcceptedRow). The inline box on a problem page carries no problem link,
which is exactly the case where the problem is not in doubt, so a missing row
slug falls through to the capture rather than being rejected. The match runs
before the dedup and lock guards, so rejecting another problem's row does not
burn them.

The same page explains the second one: nothing descriptive is on /my. Title,
tags, rating and statement are now captured at submit time and carried across
the navigation in cl_cf_pending_meta, then merged with whatever the live page
can still see. _extractMetadata no longer substitutes the slug for a missing
title — that substitution is what made an empty read look like a successful
one, and it would have defeated the merge.

Also fixed: runtime and memory were read by column index, which on /my is the
language and the verdict, so both were reported wrongly rather than left out;
they now come off .time-consumed-cell / .memory-consumed-cell of the judged
row and are null when absent. The README **Source:** link was hand-assembled
into a /contest/… URL and now goes through cfProblemUrl. And the two settings
screens disagreed about whether Codeforces was on — the platform card read
"off" while the Codeforces section read "on", losing solves either way round.

Status is now beta, not stable. The flow is correct by construction and
covered by 22 new unit tests, but none of it has been confirmed against a live
contest judge, and that is a different claim.

Gates: lint clean, prettier clean, 1012/1012 tests across 211 suites, build ok.
…exist

The self-hosted streak badges are described in Settings -> Gamification as
working in private repositories. They can: the SVGs are committed to badges/
in the repository itself, and a README can address them from there. What the
README was handed instead was https://{owner}.github.io/{repo}/badges/..., a
Pages URL assembled from the owner name whether or not Pages had ever been
enabled -- and a private repository cannot serve Pages at all. On exactly the
repositories the panel promised, every badge was a broken image.

badge-svg.js already had the correct fallback: with no base URL it addresses
the badges relative to the repository. It was unreachable because the caller
always synthesised a URL. infra-builder.js now passes the Pages URL only when
Pages is known to be serving the repo, and "" otherwise.

Also corrects two claims that would leave a fresh self-hosted deployment
broken or misconfigured:

  - wrangler.toml.example declared static files with [site] bucket, which
    binds __STATIC_CONTENT, while worker/src/index.js serves them through
    env.ASSETS. Copying the example and deploying it 404s the landing page,
    the comparison page and every other static route. Verified against
    production first: the comparison route answers 200 and its .html form
    307s, which is the redirect signature of the modern [assets] binding.
  - The example and the Worker header both named the pre-OAuth
    CODELEDGER_GH_APP_* secrets as canonical and listed SESSION_SECRET as
    optional. It is required -- sign-in returns 500 without it.

Tests: 1013 pass. The new one pins the relative fallback for {}, {pagesUrl:
""} and {pagesUrl: null}, asserting no github.io address leaks into the
markdown.
The landing page contradicted itself and undersold the build:

  - The hero listed three coding platforms and the stat strip said "3", while
    the support matrix, the meta description and the structured data all said
    five. NeetCode and takeuforward shipped; the top of the page never noticed.
  - Streaks with self-hosted badges and the friend comparison are both in the
    page's own JSON-LD featureList, and neither appeared anywhere a visitor
    could read. There was also no link to /compare from any page on the site,
    so a deployed, working route was unreachable except by typing it.
  - The import feature card still named LeetCode and GeeksForGeeks only, after
    the Codeforces profile import landed.

Adds two feature cards, a FAQ entry answering what a friend can actually see
(nothing is written to their repository, nothing notifies them, and following
is one-directional), a footer link to /compare, and the two missing chips.

/compare is deliberately NOT added to sitemap.xml -- it sets
"noindex, follow" on purpose, because a crawler following shared links would
create pages about strangers. Listing it would contradict that.

Second fix: the site stated the version twice, in the hero badge and in
softwareVersion, and both were hand-edited. sync-manifests.js now rewrites
them alongside the manifests and exits non-zero if either pattern stops
matching, so it cannot silently no-op through a release. Verified both
substitutions land against the real file before wiring it in.

Gates: lint clean, prettier clean, 1013 tests pass.
…tioned

neetcode.io and takeuforward.org have been host permissions and content-script
matches in both manifests since those handlers shipped. All three store
listings still described three platforms and justified three hosts. The last
Chrome rejection was "Inaccurate Description", so an undeclared host is the
exact failure mode to avoid on resubmission.

Chrome, Edge and Firefox copy now:

  - name all five platforms in the summary, the single-purpose description,
    the feature list, the keywords and the data-usage table
  - say which are stable and which are beta, and that committing a takeuforward
    solve needs TUF+ because that is where its judge lives
  - carry the whole net-tap disclosure: it wraps fetch AND XMLHttpRequest, only
    on those two hosts, forwards a body only when the URL matches the fixed
    ENDPOINTS list, never forwards request headers, modifies nothing -- and why
    takeuforward's problem-metadata endpoint is on that list (the API returns
    "Subscribe to TUF+" in place of difficulty and topic_tags without the
    page's own bearer token). Points reviewers at src/content/net-tap.js.

Two accuracy fixes found while writing it:

  - AMO said streak badges are refreshed "by a GitHub Action inside the user's
    own repository, not by the extension". The extension writes them itself in
    the same commit as the solve; the Action only covers days with no solve,
    and is not installed in a private repo unless asked for.
  - All three said "two further alarms" for the queue drains. There are three
    (AI review, code recovery, self-heal), and the hourly toolbar-badge redraw
    was not mentioned at all. Counted against QUEUE_ALARMS in service-worker.js
    and BADGE_ALARM in gamification-service.js.

submission.md's manual test pass now covers NeetCode, and asks the tester to
record whether the TUF+ leg was exercised or skipped rather than leaving it
implied.

Gates: 1013 tests pass; prettier clean on every file touched.
Promotes the Unreleased section to [1.8.0] and carries the version into both
manifests and the landing page's two strings via dev/sync-manifests.js.

Minor rather than patch: NeetCode and takeuforward are new tracked platforms,
the Codeforces profile import is new, and every AI surface now receives the
aggregate behaviour profile.

Also drops the last hardcoded "1.7.0" from the three places that quote a
telemetry payload as an example -- the Chrome and Edge data-usage tables and
the privacy page. They are illustrations, not a version claim, so they now
read x.y.z and cannot go stale again.

Gates: lint clean (286 advisories, no fatals), 1013 tests pass, sync
regression clean, prettier clean, both zips and the source archive build.
The proposal was to read the tier off the GitHub OAuth identity. That cannot
work: the GitHub account is a different account on a different site, and no
amount of scope on it says anything about a takeuforward subscription.

It falls out of a response we already parse. /v2/plus/problem/{slug} answers
200 to anyone and substitutes the literal string "Subscribe to TUF+" for
difficulty and topic_tags when the request is not a subscriber's -- the same
sentinel readProblemMeta already strips so it never reaches a commit. Seeing
it is proof of the free tier; seeing a real difficulty is proof of the other.
So readSubscriptionTier reads the body the net-tap already delivers: no new
request, endpoint or permission, and the answer settles the first time a TUF+
problem page is opened.

An absent field is reported as unknown rather than guessed either way, and the
handler writes the tier only when it changes -- this runs on every problem page.

The platform card then says which side of the paywall you are on. Two things
it deliberately does not do:

  - auto-disable takeuforward_enabled on the free tier. A toggle turned off
    on the user's behalf stays off after they subscribe, and one left on
    costs nothing while free, because the judge it listens for never fires.
  - re-check per sync. A subscription does not change between two syncs, and
    a check that cannot fail is not a check.

Gates: lint clean (288 advisories, no fatals), 1028 tests pass, prettier clean.
The profile import reads every accepted submission out of user.status, which
states the title, tags and rating but has no field for the problem text. So a
back catalogue of hundreds landed with hundreds of empty README sections, and
self-heal skipped all of them: HEALABLE_PLATFORMS excluded Codeforces on the
grounds that "the only way to read one is to open the page".

That conflated two different things. There is no statement API, but the page
is public and the worker can GET it -- the same kind of request the LeetCode
and GeeksForGeeks fetchers already make, with no tab opened and nothing on
screen. cfProblemUrl already builds the address from the stored slug.

The real blocker was parsing: an MV3 service worker has no DOMParser. So
extractStatementHtml walks the markup, counting <div> against </div> from the
problem-statement div to its matching close. An unbalanced document returns
null rather than a prefix -- a truncated response or one of Codeforces'
challenge interstitials is a failed attempt, retried on the existing backoff,
not half a statement committed to the user's repository as if it were whole.

What it returns is the div's inner HTML, which is exactly what the content
script stores when it captures a statement at submit time, so a healed problem
and a live one render identically. Tags come off the same page and use the
same rating filter as CodeforcesHandler._extractTags, which closes the other
healable field for the few problems user.status tags with nothing -- without
it, those would burn all five attempts refetching a statement already filled.

Title and difficulty are deliberately not scraped: the import states both from
the API, and second-guessing that gains nothing.

Also corrects the two places that repeated the old exclusion -- the Advanced
settings copy and the comment above the AI problem-summary prompt. The summary
path itself is unchanged and still correct: it is asked only when no statement
was recorded, which is now true of fewer Codeforces problems.

Gates: lint clean (288 advisories, no fatals), 1028 tests pass (16 new),
prettier clean, sync regression clean.
`enableGitHubPages` wrote a guessed `https://{owner}.github.io/{repo}/` into
`github_pages_url` whenever the follow-up GET on the Pages API failed. That
request fails whenever Pages was never provisioned, and it *necessarily* fails
for a private repository on a free plan, where Pages is not offered at all.

`infra-builder` then feeds that key to `badgeMarkdown` as the base for every
badge `src`, so the README filled with 404s. The comment at infra-builder.js:185
asserted the opposite — that the key is only written once Pages is actually
enabled — which is why the earlier private-repo badge fix was silently
defeated: badge-svg already falls back to repo-relative paths when no URL is
supplied, and its unit test still passed while production violated the
precondition.

Record only a URL GitHub itself returned, and drop the two remaining guessed
fallbacks so the README links to the repository instead.

The onboarding modal also now reads the account's plan, at no extra cost: it
already made a `GET /user` for the token scopes and threw the body away, which
is exactly where `plan` lives. An absent `plan` is treated as unknown rather
than as free — the same rule the scope helpers follow — so a token that cannot
read private profile fields never switches the option off for a paying user.
The extension said in four places that committing a takeuforward solve needs a
TUF+ subscription "because that is where its judge lives". That was an
inference, not an observation. What the live API shows is:

  - `/v2/plus/problem/{slug}` answers 200 to anyone and substitutes the literal
    "Subscribe to TUF+" for difficulty and topic_tags. Observed.
  - `/v1/plus/judge/submit` and `/check-submit` answer 401 TOKEN_MISSING to an
    anonymous request. That proves a *login* is required. It does not settle
    whether the subscription is, despite the `plus` in the path, and no account
    of either kind was available to check.

So the platform card, the api.js header, the toggle description, the three
store listings and the changelog now state the redaction, which is observed,
and the fact that takeuforward's own editor is part of TUF+ while the free
sheets link out to other sites, which is visible on the site. Nothing asserts
what the judge does for a free account.

Nothing is gated on any of this — the tier line is informational — so the
behaviour is unchanged either way.
Milestone progress compared its topics against a problem's tags after
lowercasing and nothing else. Both sides were assumed to be lowercase-hyphenated
slugs. Only one side is:

  - templates write slugs — `hash-table`, `depth-first-search`,
    `heap-priority-queue`
  - every handler stores the tag text the site displays. LeetCode reads
    `topicTags[].name`, so "Hash Table" and "Depth-First Search"; NeetCode and
    takeuforward pass their own tag text through unchanged; `resolvePrimaryTopic`
    likewise returns a display name, so the folder-topic fallback had the same
    problem.

"hash table" never equals "hash-table", so every multi-word milestone in every
shipped template scored zero for as long as the feature has existed. The
accidental single-word cases — Array, Greedy, Trie, Stack — worked, which made
it look inaccurate rather than broken. The file header in roadmap-templates.js
warns about exactly this failure ("a milestone whose subtopics are prose scores
zero forever"); it was the templates' own vocabulary that tripped it.

Fold both sides to one slug key before comparing. That also joins the platforms
up, which is the point: "Heap (Priority Queue)", "Heap Priority Queue" and
`heap-priority-queue` are one key, so a solve counts towards a milestone
whichever site it came from and however that site spells the topic.

Adds NeetCode roadmap order and Striver A2Z order templates, following each
site's own published topic sequence. They track the ordering, not the curated
problem lists — a milestone is met by any solve tagged for that topic on any
platform, which is also the more useful behaviour given both sheets link out
elsewhere to solve. Titles and blurbs say so rather than implying otherwise.
Covers the three preceding commits. Also corrects the same overstated
takeuforward judge claim where it had already been written into the 1.8.0 and
Unreleased sections — 1.8.0 is tagged locally and unpublished, so the text is
still correctable rather than a shipped claim.
The changes documented under [Unreleased] in docs/CHANGELOG.md. Highlights:

- knowledge graph rebuilt on a vendored vis-network bundle: DS/algo/domain
  axes, camera and rotation fixes, vertical legend, sidebar zoom and hide,
  URL-restored filters/search/selection, dark dropdowns, and a polish pass
  (data-derived platform filter, active-filter tint, search clear button)
- the AI can read the graph: a /graph digest command on every chat surface,
  attached automatically when a question is about progress, weak spots or
  what to practice next
- topic proficiency decays with neglect, with configurable half-life and
  regain-solves knobs
- broken imports can be fixed by hand on every platform, and the problem
  dialog Edit tab now edits every stored field with per-platform link
  verification
- stuck commits surface as a toolbar badge and a library banner with a
  retry button
- solved GFG problem pages offer an Add to Library button
- manifests regenerated so host_permissions match the generator order
Prettier drift is mechanically fixable, so the verify job now fixes it: on
a push, npm run format runs and any resulting diff is committed back to
the branch by the actions bot before the remaining gates run against the
healed tree. A GITHUB_TOKEN push does not trigger another workflow run and
Prettier is idempotent, so the heal cannot loop. Pull requests keep the
blocking format:check because a fork token cannot push back.

This is what turned run 31528331335 red: seven files of drift failed
format:check and took the whole pipeline down with them.
Two more gates reject drift a script can fix: the version copied from
package.json into the manifests and landing page (sync-manifests.js) and
the host_permissions generated from the handlers DOMAINS exports
(generate-manifest-domains.js). Run both in the auto-heal step, before
Prettier so anything they write gets formatted, and carry the result back
in the same bot commit. A sync-manifests failure still fails the run --
a landing-page pattern that stops matching is breakage, not drift.
Onboarding sets the repository homepage once, to the Pages URL GitHub
reported at creation time — always the generic {owner}.github.io/{repo}
address, because a custom domain can only be configured after the site
exists. GitHub updates the Pages html_url when a CNAME lands but never
the homepage, so the About link — the most visible address the repo
shows — pointed at the retired generic address forever, while the README
and badges (which re-read the Pages URL daily) had long since moved on.

The daily _refreshPagesUrl recheck now also syncs the homepage. The sync
runs on every live recheck rather than only when the URL changes, so a
homepage left stale before this existed still gets corrected. The guard
is a pure exported function, homepageNeedsSync: only a homepage
CodeLedger plausibly wrote is replaced — empty, the previously stored
Pages URL, or the generic Pages address — and a homepage the user typed
themselves is never touched. A failed PATCH is logged and swallowed; the
homepage is cosmetic and must never cost the commit it rode in on.
Extension detection on the landing page never worked in Firefox and was
unreliable everywhere else. Three separate faults:

- Firefox Xray isolation makes a content-script-created event detail
  read as null from the page, so every handshake arrived empty — and the
  page treated any handshake, empty included, as final, which also
  switched off its DOM-marker fallback. The detail is now exported into
  the page compartment (cloneInto on Firefox), and an empty handshake no
  longer counts as an answer.
- The page attached its handshake listener only after an awaited config
  fetch, so on a slow connection the content script could exhaust its
  retries before anyone was listening. The listener is armed first.
- library/library.html was not web-accessible, so even a handshake that
  landed carried an address the browser refuses to let a web page
  navigate to. Both manifests now expose it — to the landing page's
  origin and nowhere else — and a click is relayed to the extension's
  background (CODELEDGER_OPEN_LIBRARY) instead of depending on page
  navigation; middle-click and ctrl-click still work as ordinary links.

Every install button becomes an Open Library control once detected.
Detection is the handshake, not a store extension ID, so a side-loaded
developer build is detected exactly as a store install is. Manifests
also carry the 1.8.0 version from dev/sync-manifests.js.
Roughly 140 unused imports, variables and functions across some seventy
files, verified locally as the 96 tsc-visible findings plus their
knock-ons. Most were harmless leftovers, but two were writes to state
nothing ever read — the floating panel tracked its copy-prompt position
without using it, and a resync path destructured a token it never
touched — and dead code of that shape is where the next real bug hides.

State setters whose value is unused keep the setter and the re-render
(const [, setX] = useState(...)). The scan's five "useless conditional"
and one "superfluous argument" findings are not enumerable outside
GitHub's quality-scan UI and are not addressed here. Formatting drift
from the sweep is normalized in the same pass.
Two unrelated complaints, one batch.

Wheel zoom: vis-network zooms a fixed 10% per wheel EVENT, ignoring each
event's deltaY, with an internal floor of 1e-5 — far below the 0.05 the
zoom buttons enforce. A smooth-scroll mouse reports one flick as dozens
of events, so one flick shrank the whole graph to a sub-pixel dot. The
page now intercepts wheel itself (capture-phase, non-passive) and applies
computeWheelZoom: delta-proportional exp scaling with deltaMode
conversion, a per-event cap so a free-spinning wheel cannot teleport the
view, the buttons' own 0.05–5 clamp, and the point under the cursor kept
under the cursor.

Achievement showcase: every earned achievement was written into the
README unconditionally, including the two that reveal AI review is on.
New setting gamificationAchievementPicks — non-array means "all earned,
future ones included"; an array is an explicit selection honoured
verbatim, empty included, which removes the line entirely. That is a
deliberate difference from gamificationBadgePicks: badges have a
separate switch to drop the row, achievements have only this. The
selection flows through readmeOptions (extension commits) and
badges/config.json (nightly Action, regenerated bundle), syncs with the
portable settings, and is edited from a checkbox card in Settings →
Gamification. badges/stats.json stays unfiltered — it is the machine
record, only the human-facing README line is a view.

Also: the achievements shelf on the report page moved below the heatmap
(it pushed the platform breakdown below the fold) and its locked tiles
fold behind a Show-locked toggle once anything is earned.

Tests: 1176 pass (8 wheel-zoom, 8 achievement-picks new).
The Party tab listed rows and ranks; now it stages a competition. All of
it computed from the same public stats.json files it already fetched —
no server, no accounts, nothing about the tab's one-sided self-reported
nature changes.

New in the pure module (src/core/party.js, synced byte-identical to the
worker copy):

- metricLeaders: crowns the leader of every metric, using compareRows'
  exact tiebreak so a crown never disagrees with the leaderboard. No
  crown for a metric whose best value is zero.
- headToHead: signed per-metric diff between two stat sets.
- catchUpDays: how many days of hitting *your own* daily target closes a
  points gap. Deliberately never guesses the friend's pace — their pace
  is not something a stats file states, and inventing it would put a
  made-up number in the most motivating spot on the page.
- achievementGap: shared / only-theirs / only-mine achievement id split;
  ids from newer builds survive rather than vanish.
- lastNDays: dense zero-filled 30-day activity window, because
  summarizeIndex only reports solve days and the gaps are the story.

New in the view (PartyView.js):

- Podium for the top three (classic 2-1-3 order), medals replacing raw
  rank numbers, and a leaders strip of per-metric crown chips.
- A "vs you" delta chip on every friend row, from stats already in hand.
- Expanding a card now opens a head-to-head duel: all five metrics side
  by side, the catch-up line when behind on points, and a two-column
  achievements duel (they-have / you-have) using the local catalogue for
  emoji and names — unknown ids render as their raw id.
- A 30-day activity strip inside the deep-read detail.

Achievement id→emoji mapping stays extension-side on purpose: importing
ACHIEVEMENTS into party.js would break its dependency-freeness and the
worker sync.

Tests: 1191 pass (15 new across the five functions).
The 1.8.0 tag went out earlier today; the wheel-zoom fix, the
achievement showcase, the shelf reposition and the Party gamification
landed hours behind it and belong to the same release — the manifests
and package.json never moved off 1.8.0. Merged the Unreleased entries
into the 1.8.0 section under their matching headings (Added, Changed,
Fixed; the release's existing Removed entry is untouched) so the tag can
be moved to this head and republished.
Everything the batch covered, in one pass:

- Party, Behaviour Bank and Privacy panels stop capping their own width;
  the empty right half of the screen was a style, not a layout constraint.
- Behaviour Bank leaves Settings entirely: its data management becomes the
  fifth "Bank" tab of the Backups panel, and PanelBehaviorBank.js is gone.
- Vacations get a management UI under Settings → Streaks — the storage API
  existed with no way to reach it — plus a live sentence in the gamification
  panel that narrates exactly what the current config does to *your* streak.
- Gamification config is clamped sane: daily target ≥ 1, freeze-earn and
  penalty multipliers ≥ 1, non-finite values fall back to defaults. A 0
  target or a 1x freeze multiplier previously made every completed day bank
  a freeze and made breaking a streak cheaper than keeping it.
- Popup: the streak bar's "Lv" now renders the level number (it was handed
  the level object), and dark mode gets a light wordmark. The extension icon
  is regenerated at all four sizes with a transparent background and white
  outline.
- Stuck solves: pending commits move to src/core/pending-commits.js, the
  library banner and a new popup chip poll every 15 s and count down to the
  next retry instead of asserting "every 10 minutes" forever.
- AI review queue banner restyled into labelled chips with a retry control,
  shown only when non-empty.
- Solutions page gains the streak strip from the dashboard.
- Analytics grows a solving punch card, a 12-month stacked difficulty
  progression, per-difficulty median solve times, and an AI-Ready Insights
  Export (copyable JSON, no titles or code). The published Pages report
  renders the same punch card and trend and embeds the same JSON as
  window.CL_INSIGHTS and a #cl-insights script block.
- Landing page becomes an installable PWA (manifest + sw.js), gated on the
  extension being detected; the service worker never touches /api/*.
- Security: puppeteer bumped to ^25.8.0, removing extract-zip and its
  unvalidated-symlink path traversal (Dependabot #89). Dev-only dependency.

src/vendor/refresh-badges-source.js is regenerated because gamification.js
is one of its embedded sources.
…UI completion

Added:
- Up next bar on the Solutions tab (src/core/next-problem.js): suggests up to
  three unsolved problems ranked from recent solves' similar-problem lists,
  active roadmap milestones and least-practised topics; solved problems are
  excluded by slug and canonical id, paid-only problems outright.
- Vacation solving counts: a vacation day that hits the daily target extends
  the streak and banks freezes; Back In Action and Weekend Warrior achievements.
- Away-gap auto-detect: three-plus zero-point days ending yesterday can be
  marked as a break with one click, making them neutral.
- Welcome page card pointing at the landing page's PWA install.
- AI review-prompt editor, master AI features switch (offered only once a
  provider is configured) and incognito-mode control in settings.
- Popup recent solves deep-link into the library modal.

Fixed:
- Modal arrow navigation: async completions (refresh, AI review, code
  recovery) verify they still belong to the on-screen problem; switching
  problems resets all transient state.
- commitMessageTemplate is finally read — solve commits fill the advertised
  {topic}/{title}/{difficulty}/{language}/{platform} variables.
- Per-platform difficulty aliases fold into normalization (GFG School/Basic
  no longer fall through to a guess).
- CF/NeetCode/TUF problems open the modal on a tab they actually have
  instead of a hardcoded "overview" that rendered "No content."
- "View conflicts" lands on the Git settings panel, not General.
- Mermaid privacy disclosure no longer offers a dead jump button.

Removed:
- Dead settings stack: SettingsView, SettingsSchema, DifficultyMapPanel,
  MirrorsPanel, getAllSettingsSchemas(), src/sidebar/sidebar.html.
- "Coming soon" mirror-provider chips (1.7.0 rule: no provider in the UI
  before its commit() works).
The library's Graph tab has no counterpart in the published report, yet
index.json already carries everything the graph needs — tags, timestamps,
difficulty, and the similar-problem suggestions. Add a self-contained canvas
knowledge graph to the report template: topic hubs coloured by the same
mastery math the extension uses (volume x recency, 90-day half-life, decay
clocked from the 2nd-most-recent solve), problem dots by difficulty, unsolved
LeetCode similars as hollow ghosts capped at 250, dashed similar edges.
Pan/zoom/hover/click-to-open, colours re-read from CSS variables on theme
flip, deterministic golden-angle layout so the same data always draws the
same picture. All inline JS is string-concatenation only — the surrounding
template literal forbids backticks and dollar-brace.
Formatting and generated-file drift (prettier, sync-manifests,
host_permissions) is healed in place before packaging, mirroring the
push-time heal in ci.yml. Real breakage still fails.
…e senders

Five findings from the adversarial review, all in the same blast radius: an
attacker who can get a GitHub token of their choosing into auth.tokens silently
repoints the user's ledger at a repository they control, and reads everything
committed afterwards.

Both manifests matched the auth worker origin as `*://`, so presence-marker.js
also ran on http://codeledger.vkrishna04.me. Anyone able to answer for that host
— hostile Wi-Fi, a DNS spoof, an ISP — could serve a page carrying their own
token. Pinned to https, and pinned again in generate-manifest-domains.js so the
next run of the generator cannot quietly undo it.

presence-marker.js then trusted every page on that origin. All three token
entry points (the DOM read, the CL_GET_AUTH_DATA reply, the postMessage
fallback) now require the /api/auth/ callback path, so a reflected-XSS or a
future user-content route on the marketing site is no longer enough.

library.js listened for CODELEDGER_AUTH from the auth worker's origin — which is
the same origin as the whole public site, and library.html is web-accessible to
it. An origin allowlist cannot fix that when the trusted origin *is* the site.
The listener was also dead: the worker posts to its own window with its own
targetOrigin, so it could never reach a chrome-extension:// page. Deleted, with
a comment recording why, and the real path (storage.onChanged) left alone.

The background's tabs.onUpdated relay matched three substrings against the raw
tab URL, so https://evil.test/api/auth/x/callback#codeledger.vkrishna04.me aimed
a 15-attempt CL_GET_AUTH_DATA probe at an attacker's tab. Parsed and compared by
origin now. CODELEDGER_AUTH_RELAY compared the sender's hostname only; it
compares origin and path.

Separately, the service worker answered ~50 message types to anything that could
call sendMessage, including content scripts on five third-party platforms. A
subverted content script could send RESET_REPO or RESTORE_GITHUB_BACKUP.
lib/message-guard.js allowlists the 23 types content scripts actually send and
gives extension pages everything; the destructive set is now unreachable from a
page. externally_connectable is unset, which is why this was not already
critical — but that is a property of the manifest, not of this code.

The guard compares scheme+host rather than URL.origin: browsers give
chrome-extension:// a real origin and Node gives "null", which would have left
the check untestable on the only path that matters.
Three generators write into one README — getRepoReadme owns the stats block,
upsertReadmeBlock owns the gamification block, and the nightly workflow reruns
the second alone. Every test exercised exactly one of them, so nothing could
see the composed document the reader actually gets.

  - the total solve count rendered three times and Easy/Medium/Hard twice, from
    two counters (indexMeta.stats and the gamification snapshot) that do not
    always agree, so the duplicate could contradict itself
  - `difficulty` was labelled solved, the same label as `solved` — wrong in
    the self-hosted SVG and the shields endpoint JSON alike, since both read it
    from the same spec
  - upsertReadmeBlock prepends when it finds no markers, and the template never
    wrote any, so the streak card landed above and outside the centered div and
    rendered left-aligned above the title
  - badges/** was missing from the deploy workflow paths filter, so a nightly
    refresh rewrote card.svg, committed it, and never redeployed Pages — which
    is where the README <img> points. The refresh job also tested only `badges`
    for changes while staging `badges README.md`.

Drops solved and difficulty from DEFAULT_PICKS (both still render when picked;
buildBadgeFiles writes all six SVGs regardless) and removes the four-column
table that restated the badge row a third time, leaving the Stats section to
the breakdowns that are genuinely additional. Header cleanup alongside: the
dashboard link and timestamp were one markdown paragraph, and the six count
shields now share one shape with Languages off the points colour and Platforms
off the grey a gamification badge uses to mean zero.

test/readme-composition.test.js is the missing coverage — it composes the two
generators and asserts on the result. Every bug above is invisible to a test
that renders either half alone.
…traffic

The AI provider settings carry an optional base URL so a user can point a
provider at Azure OpenAI, LiteLLM or their own gateway. That URL decides where
the solution is posted and where the API key is sent in the Authorization
header. Settings are not a user-only input: `settings-sync.js` merges them from
`sync.json` in the ledger repository, and `backup-manager.js` merges them from a
restored snapshot.

`isPortableSetting` gates both directions — the pull loop calls it before
applying each key — and `PORTABLE_PREFIXES` waved `openai_endpoint` through on
the strength of its `openai_` prefix. `test/settings-portability.test.js`
asserted exactly that and passed, so this is documented rather than inferred.
One line in a repository the user does not solely control, or in a backup file
they were handed, and every later review, every model-list refresh and every key
test went to the attacker with the key attached.

Endpoint overrides are now local-only in both directions: `aiEndpoint` and any
`*_endpoint` key is refused by `isPortableSetting`, so it is neither written to
the repository nor accepted from it. Syncing it buys very little, so refusing it
outright is more honest than syncing it and ignoring it on the way back in. Not
done in `SECRET_SUFFIXES` — an endpoint is not a credential and that list's name
would stop being true — nor in `CRITICAL_KEYS`, which is inbound-only and would
have left the outbound write in place.

`backup-manager.js` had two copies of a denylist doing the same job. Both are
replaced by `isPortableSetting`, which also closes a second hole in them:
`!k.includes("key")` is case-sensitive, and `openai_apiKey` is not spelled that
way, so a backup could carry the key itself.

New `src/core/ai-endpoint.js` is the point-of-use backstop for a value that gets
in some other way, and the single home for the resolution chain the six handlers
had each copy-pasted. It rejects anything that is not `https:`, or `http:` on
loopback (Ollama's shipped default), and falls back to the built-in endpoint.
The module comment states what it does not do: it checks the scheme, not the
host, and `https://evil.example/v1` passes. A host allow-list narrow enough to
mean anything would break the custom-gateway feature it guards.

`model-fetch.js` needed the same guard and is the likelier trigger — it fires on
a settings-panel render, not on a solve. The override is normalised once at
function entry, because each of the eight provider branches joins the raw
parameter to its own models path; a check inside the `epFor` helper looked right
and covered one of them. `testProviderEndpoint` returns an explicit error rather
than silently testing the shipped endpoint and reporting a pass for a URL the
extension would never call.

Regression tests: 26 across four files, covering the gate itself, both entry
paths, and all three sinks. Verified failing without the fix.

Noted, not touched: `CONSTANTS.SK.AI_ENDPOINT_OVERRIDES` is declared and
referenced nowhere; `testProviderEndpoint` has no caller in `src/`, and builds
`"\tags"` where it means `"/tags"`; `settings.aiEndpoint` is read by five
handlers and written by nothing — kept and validated so a value from a much
older build keeps working.
…ledger

Three findings from the audit, all reachable without any credential:

  1. The knowledge-graph tooltip in the published Pages report interpolated
     platform names into innerHTML unescaped. They come from index.json, which
     is repository content, and the report is served from the user's own
     github.io origin. The problem title beside them was escaped from the
     start, which is what made the gap easy to miss. Audited all 14
     interpolation sites in pages-template.js rather than the reported one; the
     other 13 were already covered, consistent with the graph section being new
     in 6f53518 and the rest of the file hardened earlier.

  2. Code recovery matched the reply to its background tab on problemId alone.
     That id travels in the recovery URL's query string, so any page on a
     matched host can be handed a link that produces a matching reply — and the
     reply is saved over the stored problem and queued for commit. Match on
     sender.tab.id, which the page does not choose. A message arriving before
     tabs.create resolves cannot be checked, so it is refused.

  3. Injected buttons answered synthesized clicks. Content scripts share the
     page's DOM, so a hostile or XSS'd platform page can call .click() on our
     buttons; the import and sync handlers read the solution and commit it.
     Ten repo-writing buttons across five files now go through onTrustedClick.

The guard is deliberately not applied everywhere a click listener exists. The
copy/paste/AI-panel buttons write nothing, and the listeners attached to the
platforms' OWN submit buttons must keep answering programmatic clicks — a site
clicking its own button is normal, and refusing it would break real
submissions. leetcode/index.js:530 and the submission-detector listeners are
left alone for that reason. The test records the one remaining unguarded
listener in those five files (opens a tab; a forged click is a popup, not a
commit) so a new unguarded one fails rather than passing silently.

Each fix was verified to be load-bearing by neutering it and re-running:
disabling the isTrusted check fails 2 of 11; disabling the sender check fails
3 of 4; the tooltip test asserts on the emitted source.

Not fixed, deliberately: the reported "validate platform against
CONSTANTS.PLATFORMS in handleBulkImport". Every importer hardcodes that value,
the extension has no externally_connectable, and path-builder.js already runs
safeSegment() on the platform path segment — the guard would guard nothing
reachable.

Pre-existing, noted not fixed: model-fetch.js:376 builds "\tags" where it means
"/tags", inside testProviderEndpoint, which has no caller in src/.

1311 tests pass, type gate clean, format:check clean.
VKrishna04 and others added 15 commits August 30, 2026 18:42
…'s reach

Two things, both about the places where our code and somebody else's share a
document.

The callback page is the one response in the worker that hands out a
freshly minted repo-scoped GitHub token through the DOM, and it carried no
CSP, no framing rule and no nosniff. It now sends
`default-src 'none'` with no `connect-src` — a script that somehow ran there
would have nowhere to send the token — plus `frame-ancestors 'none'`,
`X-Frame-Options: DENY` and `nosniff`. The inline style and script carry a
nonce minted per response; a static hash cannot work, because the script
embeds the token and so differs on every response. Six tests in
worker/test/auth.test.js cover it, and all six fail when the header is
removed.

The MAIN-world net tap is the other case, and it is not fixed. NeetCode and
takeuforward render the verdict out of a JSON response an isolated-world
script cannot see, so `content/net-tap.js` runs in the page's world to wrap
`fetch` and `XMLHttpRequest` there. Sharing a world means sharing a message
channel: a compromised page on either host can post a well-formed accepted
submission and have code of its choosing committed to the user's repository.

A shared secret does not close that. Both halves would have to agree one over
`window`, where every `message` listener on the page hears it and
`event.ports` is readable by all of them, and `content/handler-loader.js`
runs at `document_idle` — after the page's own scripts — so there is no
moment when a handshake would be private. Building one anyway would look like
a defence and be none, so there isn't one.

What does hold is the injection scope, so test/net-tap-scope.test.js pins it:
exactly one MAIN-world script, exactly `*://*.neetcode.io/*` and
`*://*.takeuforward.org/*`, not all_frames, no content script matching every
site, endpoints allow-listed before anything is read, no header access, no
wildcard target origin. Widening any of those is a one-line change in a file
nobody reads closely.

docs/openapi/paths/worker.yaml documents the new response headers, and its
200 description no longer says the page posts to an opener — it does not, and
the prose above it already said so.
Two debug lines truncated a secret instead of redacting it.

`setAuthToken` logged the first twenty characters of the GitHub OAuth token.
That is `gho_` plus sixteen characters of the token itself — enough to confirm
the provider, confirm the value is real, and shorten a search. It now logs the
length.

`APIKeyPool.markFailed` logged the first eight characters of the key it was
cooling down. On a vendor key that is almost entirely the vendor's own marker
(`AIzaSy`, `sk-proj-`) and discloses close to nothing; on an opaque gateway key
it is eight real characters. It is fixed because the value was never the useful
part of the line — `getNextKey` has already logged which index it handed out —
not because it was dangerous, and the changelog says so rather than claiming a
severity the code did not have.

Both lines still report that the thing happened. Redacting a value should not
turn into deleting the line, which trades one debugging problem for another,
so test/secret-logging.test.js asserts both halves: no six-character run of the
credential reaches the console, and a line still names the write and the
cooldown. It drives the real code with the real console captured rather than
reading the source, so a future line that leaks the value by another spelling
fails too. Both assertions fail against the previous code.

While there: `markFailed` is called as `markFailed(key, err?.status)`, so a
network error with no status reached the new line and would have read "after
undefined".
…eeping

Sync reads index.json back out of the ledger repository and stored what it
found there verbatim. That file is repository content: hand-editable, writable
by a collaborator or by anyone holding a leaked token, and readable from any
repo a user is persuaded to sync from. Two of the fields it carries are not
data about a solve, they are instructions to this extension.

  _committedPaths is the list of files a problem last wrote. The maintenance
  pass deletes every path in it that the problem no longer writes, so a record
  naming .github/workflows/release.yml has that file deleted from the user's
  repository on the next Resync All. buildIndexJson publishes the field, so the
  round trip was already open.

  _conflictResolvedAt suppresses conflict detection for a problem while a
  resolution is pending a push. Dated far enough ahead it suppresses it
  permanently, keeping one record from ever being reviewed.

Strip both on the way in, by namespace rather than by name: everything the
extension stores on a record for itself is underscore-prefixed, so a field
added later is covered without anyone remembering to come back here. An
allow-list would fail the other way and silently drop whatever a future version
legitimately adds. Nothing is lost — _inferCommittedPaths already re-derives
committed paths from the actual remote tree, which is the ground truth the
stored copy is only a cache of.

The strip is at the ingest boundary, not at the write boundary in applyImport,
and that is deliberate. applyImport also receives keep-local choices from
conflict resolution, whose _committedPaths is this device's correct record;
stripping there would destroy it.

Also fixes a silent overwrite. Matching is by commit key (id::lang) but the
problems store is keyed on id alone, and saveProblem merges {...existing,
...problem}. A remote record differing only in language was therefore
classified remote-only, skipped conflict detection entirely, and then landed in
the local record's slot and replaced its code. Match on the storage slot as
well, so a collision goes to the user.

That reclassification contradicted an existing test asserting the record was
"a separate entry, not a conflict". The intent was right; the mechanism could
never deliver it, since one id is one row. The test now asserts the conflict
and says why, and the changelog says plainly that the two languages still
cannot coexist — that is a storage-key change, not a sync change.

Not fixed here, and not claimed: no size cap on index.json (resource
exhaustion, out of scope), and no path-traversal hardening in path-builder.js,
which safeSegment already closes.

A malformed entry in the problems array is now skipped rather than crashing the
import — the debug sample line dereferenced .id on the first null.

Four regression tests, each verified to fail against the unfixed code.
…ad of two

Closes out the security pass with the part that is not code.

docs/THREAT_MODEL.md is new. The first half lists what is actually defended and
every claim in it is covered by a test that fails without the defence — path
segments through safeSegment(), the bookkeeping-field strip on index.json ingest,
the signed OAuth state and the callback CSP, the fail-closed webhook, credentials
out of the logs, the trusted-click requirement on the ten buttons that write to a
repository, the recovery-tab check, the local-only AI endpoint, the sanitiser, and
the net-tap scope tests.

The second half is the reason to publish it. It says without softening that a
compromised NeetCode or takeuforward page can forge an accepted submission and
have code committed, that no shared secret fixes this because net-tap.js is
MAIN/document_start while handler-loader.js is ISOLATED/document_idle so no
handshake is ever private, and that a nonce there would be theatre. It also
states that any compromised platform page can fabricate a solve through the DOM,
that tokens and keys are readable by anyone with the OS profile, that a leaked
token reaches every repository in the granted scope rather than only the ledger,
that index.json is unsigned and uncapped, that the AI endpoint check validates
the scheme and not the host, and that the OAuth flow trusts whoever runs the
worker. Residual risks are ranked, and the two that would need a design change
say what that change is.

SECURITY.md existed twice and the copies disagreed. The .github one — the copy
GitHub surfaces in the Security tab and in the report flow — promised 72h
acknowledgement, 7-day triage and a 14-day fix estimate for a one-person project,
and sent reporters to a Chrome Web Store listing that does not exist (nothing is
published; docs/store/submission.md records all three stores as unsubmitted). The
root copy said honestly that no response time is guaranteed and was never shown
to anyone. Consolidated into the .github copy, keeping the honest expectations,
adding sync ingest to scope, and pointing at the threat model; the root file
redirects to it.

PRIVACY.md claimed extension storage means "other sites and other extensions
cannot read it". The storage API guarantee is real but narrower than that
sentence: an extension with debugger access, or one injecting into the same
pages, has routes the API does not close. Narrowed to what the browser actually
enforces.

PanelGit.js referenced a handleOAuthMessage that exists nowhere; replaced with
the path the token really takes.
Two features that share a vocabulary and therefore ship together.

The roadmap engine reads the ledger and answers three questions in the browser,
from the same index.json everything else reads — no server, no upload:

  - what to fix first (Analytics), at most three findings, each carrying the
    number behind it, ranked absent-foundation before absent-core before an
    Easy-heavy mix before a topic solved once and never revisited
  - the plan (Behaviour Bank -> Roadmap), ordered by what each topic is built
    from rather than by how often it comes up, with revision scheduled at 7, 21
    and 60 days and an explicit statement of what falls off the end when the
    work does not fit before the interview date
  - where the gaps are, now ranked by tier and dependency position instead of by
    a JavaScript object's key order, which had been handing the first three of
    twelve slots to Suffix Array, Data Stream and Iterator

The topic hierarchy makes the tags a tree: 89 topics under 19 families, every
canonical name the extension already knew placed exactly once. It is a third
relation, deliberately separate from the two that existed — an alias fold is
destructive, a prerequisite is an ordering, and containment is neither. Tree is
therefore not filed under Graph: that would invert the learning order and absorb
every tree solve into Graph, hiding the zero the gap report exists to surface.

Every placement is overridable from Settings -> Platforms -> Topic Hierarchy and
stored in settings.topicParents, so the user and the self-heal pass can both
correct it. The dropdown never offers a topic's own descendants, so a cycle
cannot be built from the UI; the walks are cycle-safe regardless, because those
settings are also editable by hand.

Roughly 54 more canonical aliases fold Codeforces' untouched tag vocabulary onto
the shared names — `sortings`, `dfs and similar`, `bitmasks` and the rest each
used to title-case into a topic of its own beside the LeetCode topic meaning the
same thing. Forward-only: a solve already stored was normalised when written,
and self-heal re-tags it on its next pass.

1438 tests pass, lint reports no fatal type errors, both dist targets build.
The repository is the database — there is no server and no account — so an
agent with a checkout and the GitHub API can do everything the extension can.
What it lacked was the two facts it will otherwise get wrong.

index.json is derived, not authoritative: the extension rebuilds it from its own
IndexedDB on every commit, so a hand-edit survives until the next solve and then
vanishes. And tag spelling is exact — `Heap (Priority Queue)`, not `Heap` — which
is the same vocabulary trap that scored every multi-word roadmap milestone at
zero for as long as that feature existed.

Carries the full index.json and problem-record schemas with their derivation
rules, the repo layout including .codeledger/*, a task-to-action table, and a
failure playbook ordered by how often each cause is the answer.
A headless render pass over all eight library tabs found the roadmap summary
reading "60 days ×2h — 123 newproblems and 40revisits queued." Thirty sites
across twenty files had the same defect.

The cause is narrower than it first looks. Testing htm against the vendored
bundle rather than reasoning about it:

  prose wrap: "answered\n  \"not found\" for"  → newline PRESERVED
  expr/text : [7,"problems"]                   → whitespace DROPPED
  text/expr : ["saw",7]                        → whitespace DROPPED

Only an expression boundary loses the whitespace. Prose wrapping onto the next
line keeps its newline, which HTML renders as a space, and attribute values keep
it too — so no class list was ever affected and the blast radius is text only.

The fix is `${" "}`, exactly what JSX uses for the identical problem. A plain
trailing space does not survive: prettier strips it, and htm treats a trailing
" \n  " as one whitespace run containing a newline and drops the run entire.

Prettier is also what generates these breaks, the first time a line grows past
the print width — so a one-time sweep would rot. `dev/find-htm-glue.js` reduces
each template to its text stream and reports the welds; `test/htm-glue.test.js`
gates on it. Five cases are recorded in ALLOWED with a reason each (the
expression carries its own `ml-*`, or sits in a `gap-*` row); a second test
fails on stale entries so a weld that stops matching gets re-reviewed rather
than silently forgotten.

Entries are keyed by rendered content, not line number, so an edit above one
does not invalidate it. Formatting after the fixes moved four tokens to the far
side of their weld — caught by the detector, which is the point of having it.

Also fixes a crash the same pass surfaced: RoadmapSection called
`buildBehaviorProfile()` as a zero-argument async function, but it is synchronous
and takes `(entries, chatStats)`. The `.then()` threw and took the section's
effect down with it; separately, the no-argument call meant `topicsUnderStrain`
was always empty, so the "Built from your history" template could never appear.
Loads the bank the way the working caller in the same file already does.
… provider

Adding an AI provider touched eight files. Six of them held a hand-written list
of the same six ids, and every one of those lists could go stale independently
without anything failing loudly:

  - privacy-disclosure.js kept its own LOCAL_AI set and FREE_TIER_AI map, so a
    new provider was disclosed as a remote paid service whatever it was
  - settings-sync.js listed portable key prefixes by hand. It stopped at
    codeforces_, so `neetcode_*` and `takeuforward_*` never travelled between
    devices — a silent failure: nothing errors, the toggles are just back at
    their defaults on the new machine
  - chat-variables.js repeated the six providers as @mention options
  - ModelStatusBar.js repeated them again as short labels
  - model-fetch.js repeated them twice more, as six near-identical branches in
    fetchModelsForProvider and six more in testAIKey

CONSTANTS.AI_PROVIDERS now carries what those lists were guessing at —
shortName, blurb, aliases, local, freeTier, and the listing wire format
(auth header, fixed headers, where the array is in the response, which fields
name a model) — and each consumer derives from it.

The model listing is the substantive change: one generic request built from the
descriptor, with LISTING_QUIRKS holding only the two things that are genuinely
per-vendor (which rows to keep, what order to show them in). The six branches
had drifted, so whether a failed listing was reported or silently swallowed
depended on which provider you had selected rather than on `throwOnError`; that
is now one rule. A listing that returns nothing falls back to the provider's
declared staticModels instead of an empty picker.

Two contradictions in the data fell out of wiring it up: deepseek declared
`supportsLiveFetch: false` while the code fetched its models live and the fetch
works, and its `staticModels` were declared but read by nothing. The flag was
the wrong half — corrected, and staticModels is now the fallback.

init.js still names each handler class: a static import is what lets the
packager see the file, and a computed import() of a path built from an id would
not be reviewable by a store reviewer reading the source. It now warns at
startup if a declared provider has no registered handler, which is the one
failure this split can produce.

test/model-fetch-providers.test.js pins the wire format per provider — a Gemini
key in x-goog-api-key, an Anthropic key in x-api-key beside the version header,
Bearer everywhere else — because the refactor is only an improvement if the
generic request still sends exactly what each vendor requires.
Adds `manual`: no API key, no account, no request. It shows the prompt the
other providers would have sent, you paste it into whichever chat you already
have, and paste the answer back.

The constraint that shaped it is that MV3 service workers have nobody to ask.
Reviews run after a solve, sometimes with the tab already closed, so a
`review()` that blocks on a paste would sit there burning the 30s timeout and
answer nothing. Two independent guards:

  - `requiresHuman` on the descriptor, filtered out of both provider chains the
    worker builds (`_buildAIReviewProviders` and `handleAIChat`), and absent
    from AI_FALLBACK_CHAIN so failover cannot reach it either.
  - `core/manual-bridge.js` throws at once when no UI has installed a resolver,
    so even a regressed filter returns immediately rather than stalling.

`core/manual-bridge.js` exists so the handler never statically imports DOM
code: `handlers/init.js` registers every AI handler and is imported by the
worker. The overlay (`ui/manual-exchange.js`) is framework-free for the same
reason floating-ai.js is — it has to work as a content script on leetcode.com,
not only on an extension page.

The four AI_CHAT call sites had four copies of the same sendMessage block and
now share `lib/ai-chat-send.js`, which is where the "does this provider need a
page" decision has to live: it is made before the message is sent.

An empty answer rejects rather than resolving. Returning "" would be committed
as that solve's AI review.

Privacy disclosure gets its own truthful branch rather than reusing `local`:
destination "your clipboard", "Nothing is sent", and it does not move the
headline tier, because the extension makes no request for it. The cost note
says whatever chat you paste into may bill you rather than claiming the answer
is free.

`supportsLiveFetch: false` gates the model listing — without it the generic
path would build the relative URL "/models" and fetch the extension's origin.

This is also the check on the previous commit's descriptor refactor: adding it
touched AI_PROVIDERS, one handler file, and the init.js registration. The
mention picker, status-bar label, settings-sync prefixes, model picker and
privacy rows all derived it.
The step list was corrected when the descriptor refactor landed, but it does
not say why a handler cannot import DOM code (init.js is imported by the
service worker, which has no document) or what `requiresHuman` and
`supportsLiveFetch: false` are for. Both are load-bearing and neither is
guessable from the step list.
…irefox 404s

The store copy was written across several releases and had drifted from the
tree in ways a reviewer checks. Each correction below was verified against a
file, not paraphrased:

- The Firefox manifest declared data collection as `["none"]`. The add-on's
  core function reads the user's solution off the page and sends it to GitHub,
  and to an AI provider once a key is entered. `websiteContent` is now
  required and `technicalAndInteraction` optional; AMO forbids combining
  anything with `none`, so it could not have stayed either way.
- submission.md opened by saying Chrome was unpublished. It has been live at
  1.4.5 since 2026-06-15 with roughly ten users, on a build predating every
  security fix since. It also now records that the next update adds `tabs`,
  which raises a new permission warning: a full re-review, with existing
  installs disabled until each user re-accepts.
- The `storage` justifications claimed nothing else leaves the browser
  (chrome.md) and that all data stays local or in the user's own repo
  (edge.md). Both were contradicted further down their own file.
- "Website content" was answered No on the grounds that code goes only to the
  user's repo. AI review sends it to a third party, so it is now Yes.
- edge.md called the opt-in counter the only call outside a host permission.
  raw.githubusercontent.com, img.shields.io and mermaid.ink are three more;
  all are enumerated now, with the four distinct raw.githubusercontent uses
  spelled out rather than only the party comparison.
- mermaid.ink was in PRIVACY.md and the live privacy panel but in no store file.
- Six alarms were listed; the service worker creates seven.
- codeledger.vkrishna04.me was justified with two uses; there is a third.
- The vendored-library list omitted vis-network.
- "Stored securely" implied encryption storage.js does not perform.
- The layout example showed lc-two-sum.md; path-builder always writes README.md.
- The topic radar was credited to the Pages report in four places; it is in the
  extension's Analytics view.
- "No third-party image service" was false of the shields.io opt-in.
- The share card was described as opening for anyone; it needs badges on, Pages
  configured and a public repo.
- takeuforward was "built and tested" like the other betas. Its judge is behind
  a TUF+ subscription we have not held, so the accepted-verdict shape is
  inferred. Said plainly now, with a reviewer note — a reviewer without one
  cannot exercise it, and "non functional" is what the listing was already
  rejected under once.

Two links were simply broken. All four Firefox install paths — README badge,
README table, landing page, EXTENSION_STORE_URLS.firefox — pointed at an AMO
listing that has never existed; they now point at the releases page and say
they are a zip. The Edge constant had the same problem.

BUILD.md named a reproducing command for preact-bundle.js and none for
chart-bundle.js, the second-largest generated file shipped. AMO source review
reads that document, and a generated file with no command behind it is the
failure it exists to catch. dev/generate-chart-bundle.js is written, wired as
`npm run vendor:chart`, and the bundle regenerated from it so the committed
file and the command agree.
The six counters on the published Pages report were literal `0`s, filled in
from index.json after load. A browser therefore showed the right numbers and
everything else showed none: `curl`, a search crawler, a link unfurl, and any
reader with scripting off. dsa.vkrishna04.me currently renders 316/176/127/11
in a browser and serves 0/0/0/0 in its HTML.

The reveal is also what un-hides `#app`, so without JavaScript the page never
left "Loading stats...".

The generator already has the counts when it writes the file — `_buildDynamicFiles`
resolves `indexMeta` for the README shields and then built the HTML without it —
so this needs no new fetch and no new pipeline, only threading the value one
level down. Using the same source as the shields also means the page and the
README cannot disagree.

- Total/Easy/Medium/Hard are interpolated into the markup and into both
  `description` and `og:description`, coerced through a non-negative integer
  guard so a malformed index.json cannot inject markup or a negative count.
- A `<noscript>` rule reveals the stats row and hides the cards that only
  script can draw, rather than leaving a spinner or a set of empty boxes.
- The runtime render still overwrites all four, so the page cannot go stale
  between infra refreshes. The two streak cells keep their placeholder: the
  streak needs the full problem list, and the summary carries ten entries.
- Onboarding writes the page before any solve exists and passes no stats, so
  it still emits zeros — there they are the true answer, not a stale one.
The published stats page still had three gaps left over from baking the
counts into its markup.

Best Streak is now baked too; Current Streak deliberately is not. The two
cells look alike and are not alike. A best streak only changes when a solve
lands, and a solve is what triggers the refresh that rewrites the page, so
the number is regenerated at the same moment it could go stale. A current
streak decays on its own: a repository that goes quiet commits nothing,
nothing regenerates the page, and a baked "12d" would go on telling crawlers
and no-script readers that a streak is running days after it ended. "—" is
not knowing, which is true; a stale number is a false claim.

Deriving it needed one fix at the source. The meta handed to the generator
carries only the ten most recent problems, which can describe at most ten
days, so the streak is computed from the full list before that slice.

The page no longer hides itself while it fetches. #app shipped with
display:none and was revealed only once index.json resolved, so a reader
with JavaScript watched a spinner sit on top of counts that were already in
the markup. The stats row is visible immediately; only the script-drawn
cards wait for data, which is also what a failed fetch now leaves on screen.

And it declares a canonical URL and ships JSON-LD. The address is the one
GitHub reports for the site — the only place a custom domain is recorded —
so when Pages is off or unchecked no canonical is emitted at all, because a
guessed {owner}.github.io/{repo} would name the wrong host on exactly the
custom-domain sites. The JSON-LD is escaped the way the commit list is, so a
value carrying a closing script tag cannot end the block.
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.3.1 to 5.4.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](nodeca/js-yaml@4.3.1...5.4.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 5.4.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot dependabot Bot added dependencies Pull requests that update a dependency file javascript Pull requests that update javascript code labels Sep 1, 2026
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatedjs-yaml@​4.3.1 ⏵ 5.4.110010086 -1394100

View full report

@VKrishna04
VKrishna04 force-pushed the main branch 2 times, most recently from f5a174c to e09d6f3 Compare September 6, 2026 17:47
@VKrishna04

Copy link
Copy Markdown
Member

Closing for now: this is a major version bump that needs a deliberate migration and manual testing, not an auto-merge. It can be revisited intentionally later; dependabot will re-propose newer majors when they land.

@VKrishna04 VKrishna04 closed this Sep 7, 2026
@dependabot @github

dependabot Bot commented on behalf of github Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

OK, I won't notify you again about this release, but will get in touch when a new version is available. If you'd rather skip all updates until the next major or minor version, let me know by commenting @dependabot ignore this major version or @dependabot ignore this minor version. You can also ignore all major, minor, or patch releases for a dependency by adding an ignore condition with the desired update_types to your config file.

If you change your mind, just re-open this PR and I'll resolve any conflicts on it.

@dependabot
dependabot Bot deleted the dependabot/npm_and_yarn/js-yaml-5.4.1 branch September 7, 2026 16:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file javascript Pull requests that update javascript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant