Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/rules/self_improvement_loop.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ If the change would touch any of these, it is **human-gated** — queue it, do N
- Anything you cannot verify locally before shipping.

### 4. IMPLEMENT
- Fresh branch off `origin/main` (`git checkout -b loop/<slug> origin/main`).
- Fresh branch off `origin/main` (`git checkout -b chore/loop-<slug> origin/main`). Use a Conventional-Commits type prefix (feat/fix/chore/docs) so the repo branch-name CI check passes — never `loop/...`.
Copy link
Copy Markdown

@augmentcode augmentcode Bot Jun 2, 2026

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 hardcodes chore/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 a feat or fix.

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

- Smallest change that makes the failure mode impossible, not just hidden.
- Match surrounding code style. Reduced-motion guard on any new animation.

Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG/pages/proto-home-v5.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,13 @@ Centralized lightweight motion tokens and added a visual polish pass across the
Self-improvement loop cycle C002 added `type="button"` to the Memory Wall sticky-delete button and the two onboarding-tour buttons (Next/Skip) — they had onclick handlers but no explicit type (implicit-submit footgun). Validated as not inside a <form>; no behavior change. e2e honesty + output-contract green.

**Commit**: `this commit`. **Author**: Homen Shum + Claude.

## 2026-06-02 — a11y: anchors-as-buttons keyboard/SR accessible (loop C003)
The "Open wiki", "Take the tour", and "Publish wiki" controls were <a onclick> with no href/role — keyboard users could not focus/activate them and screen readers announced them as links. Added role="button" + tabindex="0" and a global Enter/Space keydown delegate for any a[role=button]. No behavior change for mouse users. e2e green.

**Commit**: `this commit`. **Author**: Homen Shum + Claude.

## 2026-06-02 — a11y: accessible names on 2 inputs (loop C004)
The notes-search and note-title inputs had only placeholders (not an accessible name for screen readers). Added aria-label to both. The other text inputs already had <label for> associations. e2e green.

**Commit**: `this commit`. **Author**: Homen Shum + Claude.
10 changes: 10 additions & 0 deletions CHANGELOG/scripts/improvement-loop.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,13 @@ Created the loop substrate (`scan.mjs` deterministic opportunity detectors + sco
Added 3 detectors (button-without-type w/ form-context safety gating, target=_blank missing rel=noopener, img missing alt) and CSS/HTML comment-masking so example markup in comments is not flagged. The cycle found 1 false positive (rejected -> hardened) and 3 real button fixes (shipped). Demonstrates the loop converging: post-fix scan is clean.

**Commit**: `this commit`. **Author**: Homen Shum + Claude.

## 2026-06-02 — Cycle C003: anchor-as-button a11y detector + fix
Added detectAnchorButtons (flags <a onclick> with no href/role). Found + fixed 3 real instances (openWiki, startTour, snPublishWiki) by adding role="button" tabindex="0" + a global Enter/Space keydown delegate. Mid-cycle I caught a self-introduced bug (inline onkeydown single-quotes broke a JS innerHTML string) and corrected it before shipping — the validate-before-ship discipline in action. Post-fix scan clean.

**Commit**: `this commit`. **Author**: Homen Shum + Claude.

## 2026-06-02 — Cycle C004: unlabeled-input a11y detector + fix
Added detectUnlabeledInputs (aware of <label for>). Found 11 aria-less inputs, validated to 2 genuinely unlabeled (9 had labels), fixed both with aria-label. Notably REJECTED expanding to home-v2/v3/v4 (superseded dead prototypes, 380+ hits) as theater — the loop ships value, not churn. Post-fix scan clean.

**Commit**: `this commit`. **Author**: Homen Shum + Claude.
11 changes: 6 additions & 5 deletions public/proto/home-v5.html
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand All @@ -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">&#128075;</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&nbsp;&rarr;</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&nbsp;&rarr;</a>
</span>
<button class="welcome-close" type="button" onclick="dismissWelcome()" aria-label="Dismiss">&times;</button>
</div>
Expand Down Expand Up @@ -3410,7 +3410,7 @@ <h2 id="kbd-title">Keyboard shortcuts</h2>
'<h2 class="notes-list-title" id="sheet-title"><span class="lock">&#128274;</span>My notes</h2>' +
'<button type="button" class="notes-new-btn" onclick="createNewNote()" aria-label="New note" title="New note (⌘N)">&#43;</button>' +
'</div>' +
'<div class="notes-search"><span>&#128269;</span><input type="text" id="notes-search-input" placeholder="Search notes &amp; #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">&times;</button></div>' +
'<div class="notes-search"><span>&#128269;</span><input type="text" id="notes-search-input" aria-label="Search notes and tags" placeholder="Search notes &amp; #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">&times;</button></div>' +
'</div>' +
'<div class="notes-list-scroll" id="notes-list-scroll">' + renderNotesList() + '</div>' +
'</div>' +
Expand All @@ -3427,7 +3427,7 @@ <h2 id="kbd-title">Keyboard shortcuts</h2>
return '<div class="sheet-head"><h2 id="sheet-title">' + escapeHtml(EVENT_TITLE) + ' &middot; Wiki</h2><span class="badge">live Convex</span><button type="button" class="sheet-close" onclick="closeSheet()" aria-label="Close">&times;</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) + ' &middot; Wiki</h2><span class="badge">not published</span><button type="button" class="sheet-close" onclick="closeSheet()" aria-label="Close">&times;</button></div>' +
Expand Down Expand Up @@ -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">' +
Expand Down Expand Up @@ -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>
Copy link
Copy Markdown

@augmentcode augmentcode Bot Jun 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In public/proto/home-v5.html (line 8997), this global keydown handler will auto-repeat while a key is held down, which can trigger multiple .click() activations (e.g., repeated “Republish”) and diverge from native button semantics (Space typically activates on keyup). It also only checks e.key === " ", so some older/alternate key values may not activate the control consistently across browsers.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

</body>
</html>
48 changes: 47 additions & 1 deletion scripts/improvement-loop/ledger.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,53 @@
"scripts/improvement-loop/scan.mjs"
]
}
},
{
"cycleId": "C003",
"at": "2026-06-02T06:51:32.941Z",
"scan": {
"total": 3,
"autoSafe": 3,
"humanGated": 0
},
"selected": {
"id": "OPP-001",
"title": "Anchor used as a button (onclick, no href/role)",
"score": 1.275,
"file": "public/proto/home-v5.html:1937"
},
"outcome": "shipped",
"note": "Added detectAnchorButtons (a11y). Found 3 anchors-as-buttons (openWiki, startTour, snPublishWiki): onclick but no href/role -> not keyboard-focusable, screen-reader announces as link. Fixed with role=\"button\" tabindex=\"0\" + one global Enter/Space keydown delegate. Caught my own bug mid-cycle (inline onkeydown single-quotes broke a JS innerHTML string) and switched to the delegate. e2e 7/7. Post-fix scan clean.",
"shipped": {
"files": [
"public/proto/home-v5.html",
"scripts/improvement-loop/scan.mjs"
]
}
},
{
"cycleId": "C004",
"at": "2026-06-02T07:40:17.585Z",
"scan": {
"total": 2,
"autoSafe": 2,
"humanGated": 0
},
"selected": {
"id": "OPP-001",
"title": "Form input without an accessible name",
"score": 2.4,
"file": "public/proto/home-v5.html (notes-search-input)"
},
"outcome": "shipped",
"note": "Added detectUnlabeledInputs (label-for aware). Initial aria-only check flagged 11 inputs; VALIDATED -> 9 have a <label for> (rejected as false positives), only 2 genuinely unlabeled (notes-search-input, notes-edit-title) -> added aria-label to both. INTEGRITY: explicitly did NOT add home-v2/v3/v4 to the loop (superseded dead prototypes with 380+ button-type hits = theater; confirmed dead via git grep — referenced only in old docs). e2e 7/7. Post-fix scan clean.",
"shipped": {
"files": [
"public/proto/home-v5.html",
"scripts/improvement-loop/scan.mjs"
]
}
}
],
"updatedAt": "2026-06-02T02:34:38.978Z"
"updatedAt": "2026-06-02T07:40:17.585Z"
}
49 changes: 49 additions & 0 deletions scripts/improvement-loop/scan.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Copy Markdown

@augmentcode augmentcode Bot Jun 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In scripts/improvement-loop/scan.mjs (line 228), the \b(onclick|href|role)\s*= checks will also match attributes like data-onclick= / data-href= because - creates a word boundary, which can introduce both false positives and false negatives. That could skew the scanner’s evidence/line targeting and drive incorrect auto-safe actions.

Severity: medium

Fix This in Augment

🤖 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');
Expand All @@ -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);
}
Expand Down
Loading