Loading...
Loading...
Use this skill when you need to verify cross-file data/asset consistency in a content-driven site — not "does it render?" (that's `web-visual-verification`) but "do the data, files, and references all line up?". Triggers on phrases like "盤點內容", "稽核資產", "對照 course-data 跟 markdown", "找缺圖", "task ID 有沒有重複", "quiz 編號 vs 硬編碼總數", "audit", "content audit", "content drift", "asset coverage", "three-way sync check", "cross-file consistency", "find missing illustrations", "資料一致性檢查", "找出該補插圖的地方", or any moment when the user senses divergence between source files and deployed data. Output is a human-readable markdown report, not a pass/fail. Pair with `web-visual-verification` for full pre-release coverage.
npx skill4agent add kevintsai1202/teaching-site-skills web-content-auditSchema authority: cross-file consistency rules (3-place sync for materials, task ID stability, quiz renumber trap) are codified in§13. Audit scripts in this skill operationalise those rules._shared/domain-primitives.mdFilename convention (English-first): audit reports land under; scripts underdata/audit-*.md.scripts/audit-*.mjs
web-visual-verification| Trait | Audit | Verify |
|---|---|---|
| Output | Markdown report (human reads) | Pass/fail (CI gates) |
| Tools | File reads, regex, JSON diffs, optionally Playwright | Playwright + asserts |
| Failure mode | Always exits 0; report tells human what's worth fixing | Exits non-zero on bad state |
| When run | Manually, at milestones | On every PR / pre-deploy |
| Mental model | "Show me what's drifted" | "Don't let this regress" |
assert.*process.exit(1)web-visual-verificationcourse-data.js:materials[]getMaterialUrl()// scripts/audit-material-references.mjs
import fs from 'node:fs/promises';
import path from 'node:path';
// Read course-data.js (via vm or regex extract)
const COURSE = await loadCourse('./course-data.js');
const report = ['# Material reference audit\n'];
for (const m of COURSE.materials) {
// 1. Does the file router know about it?
const routerCovers = await checkRouterCoverage(m.name);
// 2. Does the actual file exist?
const fileExists = await checkFileExists(m);
// 3. Is it referenced in any unit's materials[]?
const referencedInUnit = findReferencingUnits(COURSE, m.id);
if (!routerCovers || !fileExists || referencedInUnit.length === 0) {
report.push(`## ⚠️ ${m.id}: ${m.name}`);
report.push(`- Router: ${routerCovers ? '✅' : '❌'}`);
report.push(`- File: ${fileExists ? '✅' : '❌'}`);
report.push(`- Referenced in units: ${referencedInUnit.join(', ') || '❌ NONE'}`);
}
}
await fs.writeFile('data/audit-materials.md', report.join('\n'));data/audit-*.md// scripts/audit-illustrations.mjs
// Walks every .chapter / .accordion-content; counts text length, image count, list density
// For each section: if (textLength > 500 && imageCount === 0) → candidate for illustration
// Output: data/illustration-audit.md (a markdown list with section titles + screenshots)web-visual-verificationcapture-*.mjswindow.COURSEindex.html// scripts/audit-corp-content.mjs
const inlined = await loadCourseFromIndexHtml('corporate-editions/client_6h/index.html');
const source = await loadSourceMarkdown('course-package/');
const report = ['# Corporate edition content audit\n'];
// Compare meta
report.push(`## Meta`);
report.push(diff(inlined.meta, source.meta));
// Per-unit: deliverables / prompts / tasks count
for (const day of ['day1', 'day2']) {
for (const unit of inlined[day].units) {
report.push(`## ${unit.id}: ${unit.title}`);
report.push(`- Prompts: ${unit.prompts?.length || 0}`);
report.push(`- Tasks: ${unit.tasks?.length || 0}`);
report.push(`- Materials: ${unit.materials?.length || 0}`);
}
}
await fs.writeFile('data/audit-corp-content.md', report.join('\n'));audit-corp-6h-content.mjstask.idquiz.idunit.id// scripts/audit-id-stability.mjs
// 1. Collect all task IDs across course-data.js — check no duplicates
// 2. Read git log for course-data.js — flag any task ID that was renamed (not just deleted)
// 3. Cross-check quiz[N].sourceUnit references actual unit IDs
// 4. Cross-check materials[].id appears in at least one unit's materials[]d2-u3-t1d2-u3-task1// scripts/inspect-docx-images.mjs
import JSZip from 'jszip';
const zip = await JSZip.loadAsync(await readFile('dist/ebook.docx'));
const images = Object.keys(zip.files).filter(f => f.startsWith('word/media/'));
console.log(`📷 內嵌圖片:${images.length} 張`);
for (const f of images) {
const size = zip.files[f]._data?.uncompressedSize || 0;
console.log(` ${f} ${(size / 1024).toFixed(1)} KB`);
}data/
├── audit-materials.md ← Audit 1
├── audit-illustrations.md ← Audit 2 (gap analysis)
├── audit/section-*.png ← ↳ supporting screenshots
├── audit-corp-content.md ← Audit 3
├── audit-id-stability.md ← Audit 4
└── audit-ebook-inspection.txt ← Audit 5# {Audit name} — {timestamp}
## Summary
- ✅ 24 of 30 materials fully wired
- ⚠️ 4 materials missing router rules
- ❌ 2 materials referenced but file missing
## ⚠️ Warnings (need attention but not blocking)
...
## ❌ Errors (likely broken in production)
...
## ℹ️ Notes (FYI)
...static-spa-conversioncourse-data.js:materials[]getMaterialUrl()const fsFiles = new Set(await fs.readdir('course-package/materials/'));
const dataMaterials = new Set(COURSE.materials.map(m => routerNameToFile(m.name)));
const routerCoverage = extractRouterRules(indexHtmlContent);
const inFsNotInData = [...fsFiles].filter(f => !dataMaterials.has(f));
const inDataNotInFs = [...dataMaterials].filter(f => !fsFiles.has(f));
const inDataNotInRouter = [...dataMaterials].filter(f => !routerCoverage.has(f));
// Emit a 3-column markdown table showing each file's coverage in each of the three places.web-visual-verificationassert.*| Trigger | Audits to run |
|---|---|
| Adding/removing a unit | id-stability, material-references |
| Adding a material file | material-references (esp. the three-place sync) |
| Renumbering quiz | id-stability + manual check of hardcoded |
| Before corporate edition handoff | corp-content (inline ↔ source) + material-references |
| Before publishing ebook | inspect-docx-images / inspect-pdf-pagecount |
| Quarterly maintenance | illustrations (gap analysis) → plan next content cycle |
assert.*audit-materials.mjsaudit-illustrations.mjsaudit-*.mjsinspect-*.mjsscripts/data/audit-*.md.json