Skip to content
Merged
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
14 changes: 14 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,20 @@ Every numbered demo (`site/src/demos/<group>/<page>/<Variant>.svelte`, id in `si

---

## Writing the Concepts and the atlas (evidence rules)

Concept pages (`site/src/content/docs/concepts/`) and the design atlas (`site/src/data/atlas/`) make claims about users. Full text: https://ui.codeatlas.com.br/docs/concepts/evidence/. Binding for every page, dataset entry and agent answer you write:

1. **Never brand as science.** The section is "Evidence", the atlas is a "design atlas". Never write "scientifically proven", "science says", "science-based", "studies show", "research proves" or "research says" (`BANNED_PHRASES` in `site/src/data/evidence/references.ts`; `packages/ui/tests/evidence-rules.test.ts` fails on them).
2. **Claim-level sourcing.** Every empirical claim about users is a `<Claim strength="established|supported|preliminary|practice|heuristic" refs="E01" scope="who, which task, which context">`. No verified source: `heuristic` (no `refs`) or cut it.
3. **Scope with every claim**: who was studied, the task, the context ("brief exposures in a lab; no preference for any style"). Never generalize beyond it.
4. **Standards are not evidence of preference.** WCAG, specs and platform guides are `practice`: what is required or recommended.
5. **No causal or demographic claims about taste.** An audience is a labelled hypothesis.
6. **Each concept page ends with `<Limits reviewed="YYYY-MM-DD">` and `<Refs ids="…">`**, in the template order: definition and vocabulary, evidence, contract, how the library implements it, anti-patterns, checklist, prompt for your AI, limits, references.
7. **Verify before publishing.** Open the DOI or URL, read the wording you rely on, and record `verified.date`, `verified.level` (`claim-read`, or `identity-only` when the text was not reachable: then no `<Claim>` may cite it) and the `excerpt` in `site/src/data/evidence/references.json`. Unverifiable sources are not added. A source's kind caps its strength (`checkReferences`): a standard or guideline is `practice`, a preprint or a vendor study is `preliminary`.
8. **No number you cannot trace.** A figure goes with its study and its conditions, or it does not go in.
9. Design inspiration sites are never named; science and standards are cited by name where they are primary sources.

## Glass and frosted styles

`glass`, `frosted` and the `liquid` recipe are opt-in surface styles (the package default is `solid`: opaque, no blur; a page with no `data-style` on `<html>` is `solid` too). The rules below apply only when blur is used: `html[data-style]` is `glass` or `frosted`, a component sets `data-ca-blur`, or a surface has `material="glass" | "frosted" | "liquid"`. An app on the default style never needs them, and a new example must not add `material="glass"` just to look finished.
Expand Down
30 changes: 28 additions & 2 deletions packages/ui/scripts/llms-full.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,27 @@ export function propsSummary(markdown: string): string {
.join("\n\n");
}

/**
* What a concept page contributes to llms-full.txt: its definition (the text before the first `## `) and the
* sections an agent acts on, in page order. The evidence, demos, limits and references stay in the page's own
* file (linked from the section), so a concept costs a few KB and the file does not grow with every page.
*/
export const CONCEPT_DIGEST_SECTIONS: readonly string[] = [
"The contract",
"Anti-patterns",
"Checklist",
"Prompt for your AI",
];

/** The digest of a concept page's markdown: the lead and the `CONCEPT_DIGEST_SECTIONS`. */
export function conceptDigest(markdown: string): string {
const [lead, ...sections] = markdown.split(/\n(?=## )/);
const keep = sections.filter((s) =>
CONCEPT_DIGEST_SECTIONS.includes(s.match(/^## (.+)$/m)?.[1]?.trim() ?? "")
);
return [lead, ...keep].join("\n\n").trim();
}

export function buildLlmsFull(opts: { site: string; docs: LlmsFullDoc[] }): string {
const parts = [
"# CodeAtlas UI: full documentation",
Expand All @@ -67,7 +88,8 @@ export function buildLlmsFull(opts: { site: string; docs: LlmsFullDoc[] }): stri
];
for (const d of opts.docs) {
const markdown = mdxToMarkdown(d.body);
const props = CORE_PAGES.includes(d.slug) ? "" : propsSummary(markdown);
const props =
CORE_PAGES.includes(d.slug) || d.group === "Concepts" ? "" : propsSummary(markdown);
parts.push(
"---",
"",
Expand All @@ -79,7 +101,11 @@ export function buildLlmsFull(opts: { site: string; docs: LlmsFullDoc[] }): stri
"",
d.description,
"",
CORE_PAGES.includes(d.slug) ? markdown : props,
CORE_PAGES.includes(d.slug)
? markdown
: d.group === "Concepts"
? conceptDigest(markdown)
: props,
""
);
}
Expand Down
75 changes: 74 additions & 1 deletion packages/ui/scripts/mdx-to-markdown.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
// MDX docs body to plain markdown (shared by the site's llms-full.txt and the MCP guide bundle).
import {
getReference,
parseRefIds,
STRENGTH_LABEL,
type Strength,
} from "../../../site/src/data/evidence/references.ts";

/**
* Raw `.svelte` demo sources by import name, for the `<Demo src={XSrc}>` examples of one MDX page.
Expand Down Expand Up @@ -37,6 +43,32 @@ function propsTable(heading: string, block: string): string {
].join("\n");
}

/** `strength="x"`, `refs="E01"` or `scope="..."` of a JSX opening tag. */
const attr = (tag: string, name: string) => tag.match(new RegExp(`\\b${name}="([^"]*)"`))?.[1];

/** "Tuch et al. 2012": the short citation shown next to a claim. */
function shortCite(id: string): string {
const r = getReference(id);
return r ? `${r.authors.split(",")[0]}${r.authors.includes(",") ? " et al." : ""} ${r.year}` : id;
}

/** The References list of a page as markdown: what each source supports, what it does not, and when it was checked. */
function refsMarkdown(ids: string, title = "References"): string {
const items = parseRefIds(ids).map((id) => {
const r = getReference(id);
if (!r) return `- ${id}: unknown reference`;
const checked =
r.verified.level === "identity-only" ? "record only, text not read" : "text read";
return [
`- [${r.id}] ${r.authors} (${r.year}). ${r.title}. ${r.venue}. ${r.url}`,
` - Strength: ${STRENGTH_LABEL[r.strength]}. Supports: ${r.supports}`,
` - Does not support: ${r.doesNotSupport}`,
` - Checked ${r.verified.date} (${checked}).`,
].join("\n");
});
return ["", `## ${title}`, "", ...items, ""].join("\n");
}

/** MDX body to plain markdown: drop imports and JSX, keep prose and code fences untouched. */
export function mdxToMarkdown(mdx: string, sources: Record<string, string> = {}): string {
const out: string[] = [];
Expand All @@ -45,6 +77,9 @@ export function mdxToMarkdown(mdx: string, sources: Record<string, string> = {})
let skipping = false; // inside a multi-line JSX tag such as <Props rows={[ ... ]} />
let propsHeading = "";
let propsLines: string[] | null = null;
let claimClose = ""; // the scope line to emit at the </Claim> that closes the open claim
let inClaim = false;
let limitsReviewed = "";
for (const raw of mdx.split("\n")) {
const fence = raw.match(/^(\s*)```/);
if (fence) {
Expand Down Expand Up @@ -92,13 +127,51 @@ export function mdxToMarkdown(mdx: string, sources: Record<string, string> = {})
}
continue;
}
const claim = line.match(/^<Claim\b[^>]*>/);
if (claim) {
const strength = attr(claim[0], "strength") as Strength | undefined;
const refs = parseRefIds(attr(claim[0], "refs") ?? "");
const scope = attr(claim[0], "scope");
const label = strength ? STRENGTH_LABEL[strength] : "Claim";
out.push(
`> **${label}**${refs.length ? `: ${refs.map(shortCite).join("; ")} [${refs.join(", ")}]` : ""}`
);
claimClose = scope ? `> *Scope: ${scope}*` : "";
// Body lines of the claim are quoted until the closing tag.
out.push(">");
inClaim = true;
continue;
}
if (line === "</Claim>") {
if (claimClose) out.push(">", claimClose);
claimClose = "";
inClaim = false;
continue;
}
const refsTag = line.match(/^<Refs\b[^>]*\/>/);
if (refsTag) {
out.push(refsMarkdown(attr(refsTag[0], "ids") ?? "", attr(refsTag[0], "title")));
continue;
}
const limits = line.match(/^<Limits\b[^>]*>/);
if (limits) {
out.push("", "## Limits", "");
limitsReviewed = attr(limits[0], "reviewed") ?? "";
continue;
}
if (line === "</Limits>") {
if (limitsReviewed) out.push("", `Last reviewed ${limitsReviewed}.`);
limitsReviewed = "";
continue;
}
// Any other JSX tag (<Steps>, </Callout>, <LinkCard ... />, <CodeGroup .../>) is layout only.
// A tag that does not end on its own line (multi-line props) is skipped until its closing `>`.
if (/^<\/?[A-Z]/.test(line)) {
if (!line.endsWith(">")) skipping = true;
continue;
}
out.push(raw.replace(/^ {2}(?=\S)/, ""));
const text = raw.replace(/^ {2}(?=\S)/, "");
out.push(inClaim ? (text ? `> ${text}` : ">") : text);
}
return out
.join("\n")
Expand Down
179 changes: 179 additions & 0 deletions packages/ui/tests/evidence-rules.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { describe, expect, it } from "bun:test";
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join, relative } from "node:path";
import {
BANNED_PHRASES,
checkReferences,
getReference,
parseRefIds,
REFERENCES,
STRENGTHS,
} from "../../../site/src/data/evidence/references.ts";
import { mdxToMarkdown } from "../scripts/mdx-to-markdown.ts";

// The editorial rules of the Evidence section (site/src/content/docs/concepts/evidence.mdx), as checks. A claim
// about users must name its source, its strength and its scope; a source must have been opened; and the site
// never presents opinion as proof.
const SITE = join(import.meta.dir, "../../../site/src");
const DOCS = join(SITE, "content/docs");

const walk = (dir: string, ext: string[]): string[] =>
readdirSync(dir).flatMap((f) => {
const p = join(dir, f);
return statSync(p).isDirectory() ? walk(p, ext) : ext.some((e) => p.endsWith(e)) ? [p] : [];
});
const pages = walk(DOCS, [".mdx"]).map((file) => ({
slug: relative(DOCS, file)
.replaceAll("\\", "/")
.replace(/\.mdx$/, ""),
src: readFileSync(file, "utf8"),
}));
/** The page without code fences and inline code: a tag written as an example is not a tag used. */
const markup = (src: string) => src.replace(/```[\s\S]*?```/g, "").replace(/`[^`\n]*`/g, "");

describe("references dataset", () => {
it("passes every structural rule", () => {
expect(checkReferences(REFERENCES)).toEqual([]);
});

it("rejects a standard presented as evidence and a source nobody opened", () => {
const base = REFERENCES.find((r) => r.id === "E07");
if (!base) throw new Error("E07 missing");
const bad = checkReferences([
{ ...base, strength: "established" },
{ ...base, id: "E98", verified: { ...base.verified, excerpt: "" } },
]);
expect(bad.some((e) => e.includes('is "practice"'))).toBe(true);
expect(bad.some((e) => e.includes("excerpt is empty"))).toBe(true);
});

it("has read the wording of every source it records", () => {
const unread = REFERENCES.filter((r) => r.verified.level !== "claim-read").map((r) => r.id);
expect(unread).toEqual([]);
// An excerpt is the wording relied on, so it is never a bare citation line.
const thin = REFERENCES.filter((r) => r.verified.excerpt.trim().length < 60).map((r) => r.id);
expect(thin).toEqual([]);
});
});

describe("site content never presents opinion as proof", () => {
// The rules page quotes the banned phrases to forbid them, so it is the one exemption.
const files = [
...pages
.filter((p) => p.slug !== "concepts/evidence")
.map((p) => ({ name: `docs/${p.slug}`, text: p.src })),
...walk(join(SITE, "components"), [".astro", ".svelte"]).map((f) => ({
name: relative(SITE, f),
text: readFileSync(f, "utf8"),
})),
];
it("has none of the banned phrases", () => {
const hits = files.flatMap((f) =>
BANNED_PHRASES.filter((b) => f.text.toLowerCase().includes(b)).map((b) => `${f.name}: "${b}"`)
);
expect(hits).toEqual([]);
});

it("never calls a section or a page 'science'", () => {
const hits = pages.flatMap((p) => {
const fm = p.src.match(/^---\n([\s\S]*?)\n---/)?.[1] ?? "";
return /^(title|group):.*\bscien(ce|tific)\b/im.test(fm) ? [p.slug] : [];
});
expect(hits).toEqual([]);
});
});

describe("concept checklists", () => {
it("are plain lists: a GFM task list renders checkboxes with no label (axe: critical)", () => {
const hits = pages.filter((p) => /^\s*[-*] \[[ xX]\] /m.test(markup(p.src))).map((p) => p.slug);
expect(hits).toEqual([]);
});
});

describe("every claim on a page is sourced", () => {
const tag = (src: string, name: string) =>
[...markup(src).matchAll(new RegExp(`<${name}\\b[^>]*>`, "g"))].map((m) => m[0]);
const attr = (t: string, n: string) => t.match(new RegExp(`\\b${n}="([^"]*)"`))?.[1];

for (const p of pages.filter((x) => /<(Claim|Refs|Limits)\b/.test(markup(x.src)))) {
describe(p.slug, () => {
it("uses a known strength, and scope plus sources on every non-heuristic claim", () => {
for (const t of tag(p.src, "Claim")) {
const strength = attr(t, "strength") ?? "";
expect(STRENGTHS as readonly string[]).toContain(strength);
if (strength === "heuristic") {
expect(attr(t, "refs")).toBeUndefined();
} else {
expect(attr(t, "scope")?.trim().length ?? 0).toBeGreaterThan(10);
expect(parseRefIds(attr(t, "refs") ?? "").length).toBeGreaterThan(0);
}
}
});

it("only cites sources that exist and whose text was read", () => {
const ids = [
...tag(p.src, "Claim").flatMap((t) => parseRefIds(attr(t, "refs") ?? "")),
...tag(p.src, "Refs").flatMap((t) => parseRefIds(attr(t, "ids") ?? "")),
];
for (const id of ids) {
const r = getReference(id);
expect(r?.id).toBe(id);
expect(r?.verified.date).toMatch(/^\d{4}-\d{2}-\d{2}$/);
}
// A claim may rest only on a source whose text was read.
for (const t of tag(p.src, "Claim"))
for (const id of parseRefIds(attr(t, "refs") ?? ""))
expect(getReference(id)?.verified.level).toBe("claim-read");
});

it("lists every source it cites in its References", () => {
const cited = new Set(
tag(p.src, "Claim").flatMap((t) => parseRefIds(attr(t, "refs") ?? ""))
);
const listed = new Set(
tag(p.src, "Refs").flatMap((t) => parseRefIds(attr(t, "ids") ?? ""))
);
expect([...cited].filter((id) => !listed.has(id))).toEqual([]);
});
});
}
});

describe("concept and evidence pages carry the template", () => {
for (const p of pages.filter(
(x) => x.slug.startsWith("concepts/") && x.slug !== "concepts/index"
)) {
it(`${p.slug} has References and a dated Limits box`, () => {
expect(p.src).toMatch(/<Refs\b/);
expect(p.src).toMatch(/<Limits reviewed="\d{4}-\d{2}-\d{2}"/);
});
}
});

describe("markdown twin of the evidence blocks", () => {
const mdx = [
'<Claim strength="supported" refs="E05" scope="Lab ratings of 50 and 500 ms exposures of homepages.">',
"Appeal ratings agree between exposures.",
"</Claim>",
"",
'<Limits reviewed="2026-10-25">',
"We did not test this.",
"</Limits>",
"",
'<Refs ids="E05" />',
].join("\n");
const md = mdxToMarkdown(mdx);

it("keeps strength, source and scope next to the claim", () => {
expect(md).toContain("> **Supported**: Lindgaard et al. 2006 [E05]");
expect(md).toContain("> Appeal ratings agree between exposures.");
expect(md).toContain("> *Scope: Lab ratings of 50 and 500 ms exposures of homepages.*");
});

it("keeps the limits, its review date and the reference with what it does not support", () => {
expect(md).toContain("## Limits");
expect(md).toContain("Last reviewed 2026-10-25.");
expect(md).toContain("[E05] Lindgaard");
expect(md).toContain("Does not support:");
});
});
Loading