You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Document and harden the Difficulty Ladder peer contract #13
Section Map works standalone for section navigation, but its difficulty-glass feature depends on Difficulty Ladder. It performs a one-time capability check for window._ddCapabilities, then subscribes to difficulty:sections-updated and consumes event.detail.sectionDifficulties.
This dependency is explained in the maintainer-only CLAUDE.md, but the README and manifest do not tell users that the difficulty visualization requires a peer plugin. Git history places the first event emission at Difficulty Ladder v0.2.0, but the currently consumed difficulty_ladder.sections.v2 payload first appears in v0.9.13. “Difficulty Ladder installed” alone is therefore not a sufficient compatibility statement.
The one-time availability gate also relies on alphabetical loading (difficulty_ladder before section_map). A delayed plugin load or reload after Section Map initializes leaves the feature unsubscribed for the session even if the provider becomes available later.
Remediation
Document Difficulty Ladder as optional for base Section Map operation and required for difficulty glasses.
Publish Difficulty Ladder v0.9.13 as the current v2 event-contract floor.
Subscribe to provider readiness/capability changes or retry the capability check on relevant screen/plugin lifecycle events instead of relying only on alphabetical startup order.
Add contract tests for absent, late-loaded, minimum, and current Difficulty Ladder providers, including malformed/older event payloads.
Add feature-scoped optional dependency metadata when the host supports it.
Opened #17 for #13 (v1.3.0). The Difficulty Ladder peer contract is now pinned in screen.js, CLAUDE.md and the README, and declared in plugin.json as an optional observer peer. Hardening: the capability handshake is retried on every 200 ms tick instead of decided once per mount (the one-shot worked only because plugin-loader.js happened to sort difficulty_ladder first within a single complete /api/plugins response — partial responses and late refetches break that), the marker must be exactly sectionDifficulty === true, payloads are normalized so non-finite fillPercentage entries and non-object containers can't reach markup (this closes a real height:NaN% path), and glassSize is allowlisted before landing in data-size. tests/screen.test.js is at 35 passing, up from 23.
Two corrections worth a reviewer's attention, both flagged by review: the payload floor is v0.12.0, not the v0.9.13 in the issue text (never released), and it is documented as a floor rather than enforced — the marker carries no version, so a below-floor peer still renders, just with the pre-v0.9.11 formula. v3 is deliberately left unconsumed; #14 owns that.
Delivered by #17, merged to main as squash commit b4ad015 (v1.3.0). Closing rather than leaving it open because every remediation item here landed; all five checked against the merged tree, not the branch:
Document the peer as optional — README.md:24 gained a "Difficulty glasses (optional)" section stating that section colors, labels, click-to-seek and wheel nudging are standalone and that only the glasses need Difficulty Ladder.
Publish a v2 event-contract floor — done, but the number changed: the floor is v0.12.0, not the v0.9.13 in this issue's text. v0.9.13 appears never to have been released; the peer's own README and CHANGOG call v0.12.0 the floor, and that is the release whose CHANGELOG documents the difficulty_ladder.sections.v2 payload shape. It is published as a documentation floor and deliberately not enforced, because the capability marker carries no version — a below-floor peer still renders, using the pre-v0.9.11 formula, which can disagree with Difficulty Ladder's own HUD rather than fail to render. README.md:43-52 says exactly that.
Retry the capability check instead of trusting load order — _smEnsureDifficultySubscription() now runs from every 200 ms _smUpdate() tick as well as on mount, so a peer that loads or reloads later in the session is still picked up. The one-shot only ever worked by accident: static/js/plugin-loader.js sorts by name || id and injects screen.js one at a time, so difficulty_ladder happened to sort first within one complete/api/plugins response — the loader skips non-ready plugins, refetches on plugin-registered, and the backend clears its registry at the start of load_plugins() and repopulates it incrementally, so partial and empty responses are routine.
Contract tests for absent / late / minimum / current / malformed providers — all present and green on the merged tree: node tests/screen.test.js is 35/35 (up from 23), including the absent provider, a foreign _ddCapabilities global, a provider appearing after mount, a v0.12.0-shaped payload, a v3-advertising provider, no/unusable detail, unreadable payload entries, glassSize markup containment, hide/show re-subscription, retry-until-bus-available, and the no-unsubscribe-handle path. Three hardening items came out of this while the tests were being written: the marker must be exactly sectionDifficulty === true, non-finite fillPercentage entries and non-object containers are normalized away (which closes a real height:NaN% path), and glassSize is allowlisted before it reaches data-size.
Feature-scoped optional dependency metadata — plugin.json declares a capabilities.difficulty-ladder block: roles: ["observer"], observes: ["difficulty:sections-updated"], compatibility: "degrade-noop", safety: "safe". The plugin schema permits additional properties and defines no dependency key, so this is the surface the spec provides for cross-plugin participation.
Two things this issue deliberately did not take on, neither of which blocks closing it:
v3 is not consumed.difficulty:sections-updated-v3 is a separate render-neutral payload emitted alongside v2; consuming it means reworking the glass rendering, which is #14. Nothing in this change sniffs schema to upgrade in passing, and the peer's transition policy exists so this plugin keeps working on the frozen v2 event meanwhile.
No standards key in the capabilities block. plugin-runtime-idempotent.v1 looks defensible — the idempotency guard is real and CI-checked — but the full declaration contract could not be verified from the spec alone, and it is not what this issue asks for.
Activity
pullfrog commentedon Sep 26, 2026
pullfrog commentedon Oct 5, 2026
Opened #17 for #13 (v1.3.0). The Difficulty Ladder peer contract is now pinned in
screen.js,CLAUDE.mdand the README, and declared inplugin.jsonas an optional observer peer. Hardening: the capability handshake is retried on every 200 ms tick instead of decided once per mount (the one-shot worked only becauseplugin-loader.jshappened to sortdifficulty_ladderfirst within a single complete/api/pluginsresponse — partial responses and late refetches break that), the marker must be exactlysectionDifficulty === true, payloads are normalized so non-finitefillPercentageentries and non-object containers can't reach markup (this closes a realheight:NaN%path), andglassSizeis allowlisted before landing indata-size.tests/screen.test.jsis at 35 passing, up from 23.Two corrections worth a reviewer's attention, both flagged by review: the payload floor is v0.12.0, not the v0.9.13 in the issue text (never released), and it is documented as a floor rather than enforced — the marker carries no version, so a below-floor peer still renders, just with the pre-v0.9.11 formula. v3 is deliberately left unconsumed; #14 owns that.
Task list (1/6 completed)
big-pickle(free) | 𝕏Document and harden the Difficulty Ladder peer contract (#13) (#17)
pullfrog commentedon Oct 5, 2026
Delivered by #17, merged to
mainas squash commitb4ad015(v1.3.0). Closing rather than leaving it open because every remediation item here landed; all five checked against the merged tree, not the branch:README.md:24gained a "Difficulty glasses (optional)" section stating that section colors, labels, click-to-seek and wheel nudging are standalone and that only the glasses need Difficulty Ladder.difficulty_ladder.sections.v2payload shape. It is published as a documentation floor and deliberately not enforced, because the capability marker carries no version — a below-floor peer still renders, using the pre-v0.9.11 formula, which can disagree with Difficulty Ladder's own HUD rather than fail to render.README.md:43-52says exactly that._smEnsureDifficultySubscription()now runs from every 200 ms_smUpdate()tick as well as on mount, so a peer that loads or reloads later in the session is still picked up. The one-shot only ever worked by accident:static/js/plugin-loader.jssorts byname || idand injectsscreen.jsone at a time, sodifficulty_ladderhappened to sort first within one complete/api/pluginsresponse — the loader skips non-readyplugins, refetches onplugin-registered, and the backend clears its registry at the start ofload_plugins()and repopulates it incrementally, so partial and empty responses are routine.node tests/screen.test.jsis 35/35 (up from 23), including the absent provider, a foreign_ddCapabilitiesglobal, a provider appearing after mount, a v0.12.0-shaped payload, a v3-advertising provider, no/unusable detail, unreadable payload entries,glassSizemarkup containment, hide/show re-subscription, retry-until-bus-available, and the no-unsubscribe-handle path. Three hardening items came out of this while the tests were being written: the marker must be exactlysectionDifficulty === true, non-finitefillPercentageentries and non-object containers are normalized away (which closes a realheight:NaN%path), andglassSizeis allowlisted before it reachesdata-size.plugin.jsondeclares acapabilities.difficulty-ladderblock:roles: ["observer"],observes: ["difficulty:sections-updated"],compatibility: "degrade-noop",safety: "safe". The plugin schema permits additional properties and defines no dependency key, so this is the surface the spec provides for cross-plugin participation.Two things this issue deliberately did not take on, neither of which blocks closing it:
difficulty:sections-updated-v3is a separate render-neutral payload emitted alongside v2; consuming it means reworking the glass rendering, which is #14. Nothing in this change sniffsschemato upgrade in passing, and the peer's transition policy exists so this plugin keeps working on the frozen v2 event meanwhile.standardskey in the capabilities block.plugin-runtime-idempotent.v1looks defensible — the idempotency guard is real and CI-checked — but the full declaration contract could not be verified from the spec alone, and it is not what this issue asks for.