What You'll Learn at This Station
HAP's Discovery: AI code generation is powerful, but AI does not know my app's architecture, my security rules, or the subtle differences between patterns that work and patterns that are technically correct but architecturally wrong. I needed layers of protection that do not depend on me being perfect.
Architecture Assumptions
AI guesses wrong
AI assumed my single-page app was a multi-page site and generated navigation links to files that do not exist.
Catching Security Patterns
innerHTML vs createElement
I caught the innerHTML problem because of AGENTS.md from the DOM lab. But the next issue was subtler.
Defense in Depth
4 independent layers
AGENTS.md prevents. ESLint detects. Husky enforces. Copilot code review catches what everything else misses.
HAP's Confession:
- I asked the AI for an about page and got
href="about.html". The AI assumed my app was multi-page. - I caught the innerHTML on the second try — only because my AGENTS.md reminded me.
- I missed the onclick property assignment completely. Grace caught it. I did not even know there was a difference.
- I realized AGENTS.md only works if the AI reads and follows it. ESLint and Husky do not depend on AI cooperation.
The Three-Layer Scene
After building my mini SPA at Station 5, I was feeling confident. I asked the AI: "Help me add an About section to my robot card app." What followed was a lesson in three layers of problems I did not expect.
HAP's Request to the AI:
"I have a robot card app. I want to add an About section. Can you write the code?"
Sounds simple. But what the AI generated revealed three layers of assumptions I had to catch — and one I could not.
Layer 1: AI Does Not Know My Architecture
The AI's first attempt created a link to about.html. But I do not have an about.html. My whole app is a single page with view functions.
// What the AI generated:
function addAboutSection() {
const aboutLink = document.createElement('a');
aboutLink.href = 'about.html';
aboutLink.textContent = 'About';
nav.appendChild(aboutLink);
}
// The problem: I don't HAVE an about.html.
// My whole app is one page. The AI assumed
// a traditional multi-page site. I caught this one right away. The AI assumed a traditional multi-page site because I did not tell it otherwise. My fix: tell the AI to make a renderAbout() view function instead.
// What I told the AI to do instead:
// "Make a renderAbout() view function —
// my app is a single page, not multi-page."
function renderAbout() {
// Show the about view, hide others
// (like the view-switching pattern from Station 3)
} 🟠 Lesson:
AI does not know my app is a single-page app unless I tell it. The AI has no context about my project's architecture — it generates code based on the most common patterns it has seen, and multi-page is more common.
Layer 2: AI Reaches for innerHTML
The AI listened. It wrote a renderAbout() view function. But it used innerHTML with a template literal to build the content.
// The AI's renderAbout() — version 2:
function renderAbout() {
const container = document.querySelector('.main-content');
container.innerHTML = `
<div class="about-section">
<h2>About Robot Cards</h2>
<p>This app lets you manage your robot collection.</p>
<button class="back-btn">Back to Cards</button>
</div>
`;
}
// It works. But innerHTML with a template literal
// is the exact pattern Grace warned about in the DOM lab.
// My AGENTS.md says: "Never use innerHTML."
// I caught this one. This works. The about section renders correctly. But innerHTML with dynamic content is exactly the pattern Grace warned about in the DOM lab. My AGENTS.md already says "Never use innerHTML." I caught this one — but only because I had that rule written down.
🟠 Lesson:
AGENTS.md saved me here. Without that written rule, I might have accepted the innerHTML version because it works. The danger is not in code that fails — it is in code that works but introduces a security risk.
Layer 3: The Subtle Issue
The AI tried again. This time it used createElement — the safe way I learned in the DOM lab. No innerHTML. I was ready to accept it. Then Grace spoke up.
// The AI's renderAbout() — version 3 (createElement):
function renderAbout() {
const container = document.querySelector('.main-content');
container.textContent = '';
const section = document.createElement('div');
section.classList.add('about-section');
const heading = document.createElement('h2');
heading.textContent = 'About Robot Cards';
const description = document.createElement('p');
description.textContent = 'This app lets you manage your robot collection.';
const backBtn = document.createElement('button');
backBtn.classList.add('back-btn');
backBtn.textContent = 'Back to Cards';
// The subtle issue:
backBtn.onclick = () => renderHome();
// ^^^^^^^
// onclick property assignment, not addEventListener
section.append(heading, description, backBtn);
container.appendChild(section);
} Grace Hopper:
"The onclick property works. But it permits only one handler per event. addEventListener permits many. The AI chose convenience. You must decide whether convenience is acceptable for your architecture."
I stared at the code. backBtn.onclick = () => renderHome(). It looked fine to me. But Grace was right — onclick is a property assignment. If anything else sets onclick on that button, it overwrites my handler. addEventListener stacks handlers — multiple things can listen to the same event.
// The correct version — addEventListener:
function renderAbout() {
const container = document.querySelector('.main-content');
container.textContent = '';
const section = document.createElement('div');
section.classList.add('about-section');
const heading = document.createElement('h2');
heading.textContent = 'About Robot Cards';
const description = document.createElement('p');
description.textContent = 'This app lets you manage your robot collection.';
const backBtn = document.createElement('button');
backBtn.classList.add('back-btn');
backBtn.textContent = 'Back to Cards';
// addEventListener — permits multiple handlers
backBtn.addEventListener('click', () => renderHome());
section.append(heading, description, backBtn);
container.appendChild(section);
} What I Caught and What I Missed
HAP's Reflection:
I caught the wrong architecture — href="about.html" was obvious because I know my app is a single page.
I caught the innerHTML — but only because AGENTS.md reminded me.
I missed the onclick. Grace caught it. I did not even know there was a difference between onclick and addEventListener.
The more I learn, the more I can see — but there is always another layer. 🟠
Defense in Depth
If Grace had not been here, how would I catch these problems? That question led me to the answer: I need layers of protection that do not depend on me being perfect.
Layer 1: AGENTS.md — Prevention
Tell the AI what to generate and what to avoid. Architecture rules ("this is an SPA, never generate new HTML files") and code rules ("use addEventListener, not onclick"). This is the first line of defense — but it only works if the AI reads and follows it.
Layer 2: ESLint Plugins — Detection
eslint-plugin-unicorn has a prefer-add-event-listener rule that flags btn.onclick = .... eslint-plugin-no-unsanitized (from Mozilla) flags innerHTML assignments. These scan code after it is written — they do not depend on the AI cooperating.
Layer 3: Husky + lint-staged — Enforcement
Already in ready-build from Week 1. Runs ESLint at commit time. Even if I ignore warnings in the editor, the commit hook blocks bad code from entering the repo. The gate does not open for code that breaks the rules.
Layer 4: Copilot Code Review — AI Reviewer
Available via CLI: gh pr edit --add-reviewer @copilot. Analyzes full repo context, not just the diff. It is like having a reviewer who never gets tired and never misses a PR.
# AGENTS.md additions for event-driven code
## Architecture
- This is a single-page app (SPA).
- Never generate code that creates new HTML files
or uses window.location for navigation.
- All "pages" are view functions that show/hide
content within index.html.
## Code rules
- Use addEventListener, not onclick property assignments.
- Never use innerHTML — use createElement and textContent.
- All event listeners use named callbacks or arrow functions
passed to addEventListener. I added these exact rules to the Robot ID Card app. You can see them in the source code on GitHub. The app uses addEventListener everywhere, createElement instead of innerHTML, and classList instead of inline styles. That is the prevention layer in action.
// ESLint plugins for event-driven code safety
// eslint-plugin-unicorn
// Rule: prefer-add-event-listener
// Flags: btn.onclick = () => { ... }
// Suggests: btn.addEventListener('click', () => { ... })
// eslint-plugin-no-unsanitized (Mozilla)
// Rule: no-unsanitized/property
// Flags: element.innerHTML = userInput
// Flags: element.outerHTML = template Grace Hopper:
"Defense in depth. You do not rely on a single mechanism. Each layer is independent. Each catches what the others miss."
Four Versions of renderAbout()
Here are all four versions the AI generated, in order. Each one got closer to correct, but each had a different kind of problem.
Version 1: Wrong Architecture
Used href="about.html". Assumed multi-page. I caught it because I know my app is a single page.
Version 2: Security Risk
Used innerHTML with a template literal. Works, but the DOM lab taught me this is a security risk. AGENTS.md caught it.
Version 3: Subtle Issue
Used createElement (safe) but onclick property (only one handler allowed). Grace caught it. I missed it completely.
Version 4: Correct
Uses createElement and addEventListener. Safe, extensible, follows the pattern from Station 1. This is the version that belongs in the app.
Defense-in-Depth Quick Reference
AGENTS.md — Prevention
Add architecture rules ("this is an SPA") and code rules ("use addEventListener, not onclick"). Prevents the AI from generating wrong patterns in the first place.
ESLint Plugins — Detection
eslint-plugin-unicorn (prefer-add-event-listener) and eslint-plugin-no-unsanitized (flags innerHTML). Scans code regardless of who wrote it.
Husky + lint-staged — Enforcement
Runs ESLint at commit time. Bad code cannot enter the repo, even if warnings are ignored in the editor.
Copilot Code Review — AI Reviewer
gh pr edit --add-reviewer @copilot. Reviews the full repo context on every PR. A second pair of eyes that never has to recharge.
Learning Objectives Checklist
Congratulations on completing all six stations of HAP's Learning Lab! Before you finish, verify you have mastered the core event-handling patterns and the principles of responsible AI use:
Event Fundamentals
- I can wire up an event listener with addEventListener and explain the three-part pattern
- I can read event.target and event.type to make smart decisions inside a callback
- I can explain the difference between event.target and event.currentTarget
Interactive Patterns
- I can switch views on a single page by toggling a .hidden class
- I can handle form submissions with preventDefault and read values with FormData
- I can use event delegation with .closest() to handle dynamic content
AI-Assisted Development
- I can identify when AI-generated code assumes the wrong architecture
- I can spot innerHTML and onclick patterns and replace them with safer alternatives
- I can describe the four layers of defense: AGENTS.md, ESLint, Husky, and Copilot review
HAP's Closing Reflection:
From "the page just sits there" to "I can make any element respond to anything, and I have layers of protection that do not depend on me being perfect."
AGENTS.md is prevention. ESLint is detection. Husky is enforcement. And Copilot code review? That is like having a Grace who never has to recharge. 🟠