-
Notifications
You must be signed in to change notification settings - Fork 3
feat(loop): cycles C003+C004 — anchor + input a11y detectors & fixes #461
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1934,7 +1934,7 @@ <h1>A disposable sidecar room for live event memory.</h1> | |
| <button type="button" class="la-toggle" id="la-toggle" data-on="false" onclick="toggleLiveAssist()" aria-pressed="false" aria-label="Toggle Live Assist private cues" title="Private cues — only you see these"> | ||
| <span class="icon">💡</span><span>Cues</span> | ||
| </button> | ||
| <a class="ev-link" onclick="openWiki()">Open wiki →</a> | ||
| <a class="ev-link" role="button" tabindex="0" onclick="openWiki()">Open wiki →</a> | ||
| </div> | ||
|
|
||
| <main class="m"> | ||
|
|
@@ -1943,7 +1943,7 @@ <h1>A disposable sidecar room for live event memory.</h1> | |
| <div class="welcome" role="status" aria-live="polite"> | ||
| <span class="welcome-emoji" aria-hidden="true">👋</span> | ||
| <span class="welcome-text"> | ||
| <strong>New to ScratchNode?</strong> This is the sidecar room: chat publicly, <code style="font-family:var(--mono);background:rgba(217,119,87,.12);color:var(--accent);padding:0 4px;border-radius:3px">/ask</code> for sourced answers, or lock the composer for private notes. <a onclick="startTour()">Take the 20-second tour →</a> | ||
| <strong>New to ScratchNode?</strong> This is the sidecar room: chat publicly, <code style="font-family:var(--mono);background:rgba(217,119,87,.12);color:var(--accent);padding:0 4px;border-radius:3px">/ask</code> for sourced answers, or lock the composer for private notes. <a role="button" tabindex="0" onclick="startTour()">Take the 20-second tour →</a> | ||
| </span> | ||
| <button class="welcome-close" type="button" onclick="dismissWelcome()" aria-label="Dismiss">×</button> | ||
| </div> | ||
|
|
@@ -3410,7 +3410,7 @@ <h2 id="kbd-title">Keyboard shortcuts</h2> | |
| '<h2 class="notes-list-title" id="sheet-title"><span class="lock">🔒</span>My notes</h2>' + | ||
| '<button type="button" class="notes-new-btn" onclick="createNewNote()" aria-label="New note" title="New note (⌘N)">+</button>' + | ||
| '</div>' + | ||
| '<div class="notes-search"><span>🔍</span><input type="text" id="notes-search-input" placeholder="Search notes & #tags..." oninput="filterNotesList(this.value)"><button type="button" class="sheet-close" onclick="closeSheet()" aria-label="Close" style="background:transparent;border:0;color:var(--ink-faint);cursor:pointer;font-size:18px;padding:0 4px">×</button></div>' + | ||
| '<div class="notes-search"><span>🔍</span><input type="text" id="notes-search-input" aria-label="Search notes and tags" placeholder="Search notes & #tags..." oninput="filterNotesList(this.value)"><button type="button" class="sheet-close" onclick="closeSheet()" aria-label="Close" style="background:transparent;border:0;color:var(--ink-faint);cursor:pointer;font-size:18px;padding:0 4px">×</button></div>' + | ||
| '</div>' + | ||
| '<div class="notes-list-scroll" id="notes-list-scroll">' + renderNotesList() + '</div>' + | ||
| '</div>' + | ||
|
|
@@ -3427,7 +3427,7 @@ <h2 id="kbd-title">Keyboard shortcuts</h2> | |
| return '<div class="sheet-head"><h2 id="sheet-title">' + escapeHtml(EVENT_TITLE) + ' · Wiki</h2><span class="badge">live Convex</span><button type="button" class="sheet-close" onclick="closeSheet()" aria-label="Close">×</button></div>' + | ||
| '<div class="wiki-shell"><aside class="wiki-toc" aria-label="Table of contents"><div class="wiki-toc-label">Source</div><a class="wiki-toc-link active">Published snapshot</a></aside>' + | ||
| '<article class="wiki-article" id="wiki-article">' + window._sn_published_wiki_body + '</article>' + | ||
| '<aside class="wiki-onpage" aria-label="Actions"><div class="wiki-onpage-label">Actions</div><a onclick="window.snPublishWiki && window.snPublishWiki()">Republish</a></aside></div>'; | ||
| '<aside class="wiki-onpage" aria-label="Actions"><div class="wiki-onpage-label">Actions</div><a role="button" tabindex="0" onclick="window.snPublishWiki && window.snPublishWiki()">Republish</a></aside></div>'; | ||
| } | ||
| if (window._sn_live && window._sn_live.eventId) { | ||
| return '<div class="sheet-head"><h2 id="sheet-title">' + escapeHtml(EVENT_TITLE) + ' · Wiki</h2><span class="badge">not published</span><button type="button" class="sheet-close" onclick="closeSheet()" aria-label="Close">×</button></div>' + | ||
|
|
@@ -3994,7 +3994,7 @@ <h2 id="kbd-title">Keyboard shortcuts</h2> | |
| h.push('<button type="button" class="toolbar-label" onclick="noteInsertText(\'#\')" title="Add tag" style="color:var(--accent)">#</button>'); | ||
| h.push('</div>'); | ||
| h.push('<div class="notes-edit-body">'); | ||
| h.push('<input type="text" class="notes-edit-title" id="notes-edit-title" placeholder="Untitled" value="' + escapeHtml(note.title || '') + '" oninput="updateActiveNote(\'title\', this.value)" />'); | ||
| h.push('<input type="text" class="notes-edit-title" id="notes-edit-title" aria-label="Note title" placeholder="Untitled" value="' + escapeHtml(note.title || '') + '" oninput="updateActiveNote(\'title\', this.value)" />'); | ||
| if (tagsHtml) h.push('<div class="notes-edit-tags">' + tagsHtml + '</div>'); | ||
| if (note.anchorType) { | ||
| h.push('<div class="notes-backlinks"><div class="notes-backlinks-label">Private anchor</div><div class="notes-backlink-row">' + | ||
|
|
@@ -8994,5 +8994,6 @@ <h2 id="kbd-title">Keyboard shortcuts</h2> | |
| } catch (e) { /* no-op */ } | ||
| })(); | ||
| </script> | ||
| <script>document.addEventListener("keydown",function(e){if((e.key==="Enter"||e.key===" ")&&e.target&&e.target.matches&&e.target.matches("a[role=\"button\"]")){e.preventDefault();e.target.click();}});</script> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In Severity: medium 🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage. |
||
| </body> | ||
| </html> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -216,6 +216,53 @@ function detectImagesWithoutAlt(surface, lines) { | |
| return out; | ||
| } | ||
|
|
||
| // Anchor used as a button: <a onclick=...> with no href and no role. Not keyboard- | ||
| // focusable and announced as a link by screen readers. Auto-safe fix: role/tabindex/keydown. | ||
| function detectAnchorButtons(surface, lines) { | ||
| const out = []; | ||
| const text = maskComments(lines.join('\n')); | ||
| const re = /<a\b([^>]*)>/g; | ||
| let m; | ||
| while ((m = re.exec(text))) { | ||
| const a = m[1]; | ||
| if (!/\bonclick\s*=/.test(a)) continue; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In Severity: medium 🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage. |
||
| if (/\bhref\s*=/.test(a)) continue; | ||
| if (/\brole\s*=/.test(a)) continue; | ||
| out.push(opp({ | ||
| title: 'Anchor used as a button (onclick, no href/role) — not keyboard/SR accessible', | ||
| surface: surface.id, source: 'a11y-scan', file: surface.file + ':' + lineOf(text, m.index), | ||
| evidence: ('<a' + a + '>').slice(0, 140), | ||
| impact: 3, confidence: 0.85, effort: 2, safety: 'auto', | ||
| })); | ||
| } | ||
| return out; | ||
| } | ||
|
|
||
| // Form input with no accessible name: no aria-label/aria-labelledby AND no <label for=id>. | ||
| // Placeholder is NOT an accessible name. Auto-safe fix: add aria-label matching purpose. | ||
| function detectUnlabeledInputs(surface, lines) { | ||
| const out = []; | ||
| const text = maskComments(lines.join('\n')); | ||
| const labelFor = new Set([...text.matchAll(/<label\b[^>]*\bfor\s*=\s*"([^"]+)"/g)].map((m) => m[1])); | ||
| const re = /<input\b([^>]*)>/g; | ||
| let m; | ||
| while ((m = re.exec(text))) { | ||
| const a = m[1]; | ||
| const typ = (a.match(/type\s*=\s*"([^"]*)"/) || [])[1] || 'text'; | ||
| if (['hidden', 'submit', 'button', 'checkbox', 'radio'].includes(typ)) continue; | ||
| if (/\baria-label\s*=/.test(a) || /\baria-labelledby\s*=/.test(a)) continue; | ||
| const id = (a.match(/\bid\s*=\s*"([^"]+)"/) || [])[1]; | ||
| if (id && labelFor.has(id)) continue; | ||
| out.push(opp({ | ||
| title: 'Form input without an accessible name (no aria-label / <label for>)', | ||
| surface: surface.id, source: 'a11y-scan', file: surface.file + ':' + lineOf(text, m.index), | ||
| evidence: ('<input' + a + '>').slice(0, 140), | ||
| impact: 3, confidence: 0.8, effort: 1, safety: 'auto', | ||
| })); | ||
| } | ||
| return out; | ||
| } | ||
|
|
||
| /** Honesty-contract-adjacent zones — recorded so the agent knows changes there are human-gated. */ | ||
| function detectSafetyGatedZones(surface, lines) { | ||
| const text = lines.join('\n'); | ||
|
|
@@ -240,6 +287,8 @@ function main() { | |
| ...detectButtonsWithoutType(surface, lines), | ||
| ...detectUnsafeBlankLinks(surface, lines), | ||
| ...detectImagesWithoutAlt(surface, lines), | ||
| ...detectAnchorButtons(surface, lines), | ||
| ...detectUnlabeledInputs(surface, lines), | ||
| ); | ||
| safetyZones[surface.id] = detectSafetyGatedZones(surface, lines); | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In
.claude/rules/self_improvement_loop.md(line 36), the example branch command hardcodeschore/loop-<slug>but the same sentence says to use a Conventional-Commits type prefix (feat/fix/chore/docs), which reads as internally inconsistent. This may lead future cycles to choose the wrong branch prefix when the change is afeatorfix.Severity: low
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.