Skip to content
Draft
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
77 changes: 77 additions & 0 deletions .agents/skills/frontend-aeo-wiki/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
name: frontend-aeo-wiki
description: Generate AEO wiki and answer-page drafts from the user-visible surface of a frontend repository, especially Next.js App Router apps. Use when turning frontend routes, pages, metadata, UI copy, README, or public docs into Kinic Wiki Markdown and /answers Markdown.
---

# Frontend AEO Wiki

Use this skill when a frontend repository should become an AI-search-ready public wiki.

## Scope

Only document user-visible product behavior.

Include:

- Next.js App Router pages and layouts
- route metadata and page titles
- UI copy visible in components used by public pages
- README and public docs
- public API behavior only when surfaced in UI or public docs

Exclude:

- database schemas
- backend-only code and internal clients
- tests, fixtures, generated files, build scripts
- secrets, environment values, private config
- hidden admin surfaces
- claims about pricing, security, compliance, performance, competitors, or roadmap unless explicitly present in visible UI, metadata, README, or public docs

## Workflow

1. Detect the frontend framework. For now, support Next.js App Router first.
2. Build a source pack from visible routes, layouts, metadata, README, and docs.
3. Generate concise Wiki Markdown:
- `/Wiki/product/overview.md`
- `/Wiki/product/screens.md`
- `/Wiki/product/features.md`
- `/Wiki/product/faq.md`
4. Generate AEO answer Markdown under `/Wiki/aeo/...`.
5. Every answer must include frontmatter with `title`, `description`, `answer_summary`, `updated`, `index: true`, `entities`, and `sources`.
6. Every answer body must cite repo-relative source paths.
7. Reject or omit unsupported claims instead of inventing product promises.

## Answer Contract

Use this frontmatter shape:

```md
---
title: What is Example?
description: Example helps users understand the visible product value.
answer_summary: Example provides the visible product experience described in the frontend.
updated: 2026-05-07
index: true
entities:
- Example
sources:
- README.md
- app/page.tsx
---
```

The body should be short, factual, and grounded in `sources`.

## Validation

Before publishing, ensure:

- slugs are unique
- required frontmatter exists
- `sources` is non-empty
- Markdown parses
- no secret patterns appear
- no unsupported claim category appears without a visible source

If validation fails, do not publish generated pages.
172 changes: 172 additions & 0 deletions crates/vfs_cli_app/src/aeo_generate/collect.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// Where: crates/vfs_cli_app/src/aeo_generate/collect.rs
// What: Collect and validate user-visible frontend source paths.
// Why: AEO generation must ignore implementation-only repository content.

use crate::aeo_generate::types::{SourceEntry, SourceKind, ValidationReport};
use anyhow::Result;
use std::collections::BTreeMap;
use std::fs;
use std::path::Path;

const MAX_SOURCE_BYTES: u64 = 512 * 1024;

pub fn collect_frontend_sources(repo: &Path) -> Result<Vec<SourceEntry>> {
let mut sources = BTreeMap::<String, SourceKind>::new();
let readme = repo.join("README.md");
if readme.is_file() {
sources.insert("README.md".to_string(), SourceKind::Readme);
}
collect_docs(repo, repo, &mut sources)?;
collect_next_app(repo, repo, &mut sources)?;
Ok(sources
.into_iter()
.map(|(path, kind)| SourceEntry { path, kind })
.collect())
}

pub fn validate_source_pack(sources: &[SourceEntry], repo: &Path) -> ValidationReport {
let mut report = ValidationReport {
passed: true,
errors: Vec::new(),
warnings: Vec::new(),
};
if !sources
.iter()
.any(|source| source.kind == SourceKind::NextAppPage)
{
report
.errors
.push("missing Next.js App Router page source".to_string());
}
if sources.is_empty() {
report
.errors
.push("no frontend AEO sources found".to_string());
}
for source in sources {
let path = repo.join(&source.path);
if let Ok(content) = fs::read_to_string(path)
&& contains_secret_pattern(&content)
{
report
.errors
.push(format!("secret-like pattern found in {}", source.path));
}
}
report.passed = report.errors.is_empty();
report
}

fn collect_docs(
repo: &Path,
root: &Path,
sources: &mut BTreeMap<String, SourceKind>,
) -> Result<()> {
let docs = repo.join("docs");
if !docs.is_dir() {
return Ok(());
}
collect_files(
&docs,
root,
sources,
|relative| relative.ends_with(".md") || relative.ends_with(".mdx"),
SourceKind::PublicDoc,
)
}

fn collect_next_app(
repo: &Path,
root: &Path,
sources: &mut BTreeMap<String, SourceKind>,
) -> Result<()> {
let app = repo.join("app");
if !app.is_dir() {
return Ok(());
}
collect_files(
&app,
root,
sources,
|relative| {
(relative.ends_with("/page.tsx") || relative == "app/page.tsx")
|| (relative.ends_with("/layout.tsx") || relative == "app/layout.tsx")
},
SourceKind::NextAppPage,
)?;
for (path, kind) in sources.iter_mut() {
if path.ends_with("/layout.tsx") || path == "app/layout.tsx" {
*kind = SourceKind::NextAppLayout;
}
}
Ok(())
}

fn collect_files(
dir: &Path,
root: &Path,
sources: &mut BTreeMap<String, SourceKind>,
include: impl Fn(&str) -> bool + Copy,
kind: SourceKind,
) -> Result<()> {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
let file_name = entry.file_name();
let file_name = file_name.to_string_lossy();
if should_skip_name(&file_name) {
continue;
}
if path.is_dir() {
collect_files(&path, root, sources, include, kind)?;
continue;
}
if !path.is_file() || path.metadata()?.len() > MAX_SOURCE_BYTES {
continue;
}
let relative = repo_relative(root, &path)?;
if include(&relative) && !is_hidden_admin_surface(&relative) {
sources.insert(relative, kind);
}
}
Ok(())
}

fn should_skip_name(name: &str) -> bool {
name.starts_with('.')
|| matches!(
name,
"node_modules"
| "target"
| ".next"
| "dist"
| "build"
| "coverage"
| "__tests__"
| "tests"
| "fixtures"
)
}

fn is_hidden_admin_surface(relative: &str) -> bool {
relative
.split('/')
.any(|segment| segment == "admin" || segment == "(admin)" || segment.starts_with("_"))
}

fn repo_relative(root: &Path, path: &Path) -> Result<String> {
Ok(path
.strip_prefix(root)?
.components()
.map(|component| component.as_os_str().to_string_lossy())
.collect::<Vec<_>>()
.join("/"))
}

fn contains_secret_pattern(content: &str) -> bool {
content.contains("-----BEGIN PRIVATE KEY-----")
|| content.contains("sk-")
|| content.contains("AKIA")
|| content.contains("SECRET_KEY=")
|| content.contains("NEXT_PUBLIC_") && content.contains("PRIVATE")
}
Loading
Loading