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
267 changes: 267 additions & 0 deletions .claude/skills/abstract-data-docs-author/SKILL.md

Large diffs are not rendered by default.

461 changes: 312 additions & 149 deletions .claude/skills/abstract-data-setup/SKILL.md

Large diffs are not rendered by default.

273 changes: 273 additions & 0 deletions .cursor/rules/abstract-data-docs-author.mdc

Large diffs are not rendered by default.

461 changes: 312 additions & 149 deletions .cursor/rules/abstract-data-setup.mdc

Large diffs are not rendered by default.

763 changes: 606 additions & 157 deletions .github/copilot-instructions.md

Large diffs are not rendered by default.

71 changes: 71 additions & 0 deletions .github/workflows/deploy-playground.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
name: Deploy playground (demo) to GitHub Pages

# Builds apps/playground and deploys it to GitHub Pages so the theme has a
# live demo URL anyone can link to from npm or the README. To enable:
#
# 1. Repo Settings → Pages → Build and deployment → Source: GitHub Actions
# 2. Push to main, or trigger manually from the Actions tab
#
# Result: https://abstract-data.github.io/abstract-data-doc-theme/

on:
push:
branches: [main]
paths:
- 'apps/playground/**'
- 'packages/starlight-theme/**'
- '.github/workflows/deploy-playground.yml'
- 'bun.lock'
- 'package.json'
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

# Allow only one concurrent deployment, skipping runs queued between
# in-progress and pending. Don't cancel in-progress runs to let them finish.
concurrency:
group: pages
cancel-in-progress: false

jobs:
build:
name: Build playground
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Build playground
working-directory: apps/playground
run: bun run build
env:
# GitHub Pages serves under /<repo>/, so set the base path
BASE_URL: /abstract-data-doc-theme

- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: apps/playground/dist

deploy:
name: Deploy
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ dist/
.astro/
.output/

# bun
bun.lock
# bun — bun.lock IS committed (text-based lockfile, needed for reproducible installs)
.bun/

# logs
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

The branded docs system Abstract Data uses across client projects. Built on [Astro Starlight](https://starlight.astro.build/) and distributed as `@abstractdata/starlight-theme` on npm. Premium polish, brand-locked surfaces, opinionated defaults.

**🌐 Live demo:** https://abstract-data.github.io/abstract-data-doc-theme/

> **Naming for AI agents:** refer to this as the **Abstract Data Documentation Theme** (the product). The npm package name `@abstractdata/starlight-theme` reflects the substrate (Astro Starlight), not the product identity.

This is a Bun-workspaces monorepo:
Expand Down
3 changes: 2 additions & 1 deletion apps/playground/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { abstractDataThemes } from '@abstractdata/starlight-theme/shiki';

// https://astro.build/config
export default defineConfig({
site: 'https://docs.abstractdata.io',
site: 'https://abstract-data.github.io',
base: process.env.BASE_URL ?? '/',
integrations: [
starlight({
title: 'Abstract Data',
Expand Down
123 changes: 115 additions & 8 deletions apps/playground/scripts/build-python-docs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,10 @@ try {
mkdirSync(outputDir, { recursive: true });
log(`${c.dim}→ generating ${cfg.modules.length} module page${cfg.modules.length === 1 ? '' : 's'}${c.reset}`);

let generated = 0;
const generatedFiles = [];
// Two-pass build: first generate every page in memory so the thin-page
// post-processor can cross-reference siblings (for "Submodules" sections
// on package landing pages), then write everything to disk.
const pages = []; // { mod, safeName, outPath, title, description, body, frontmatter }

for (const mod of cfg.modules) {
const safeName = mod.replace(/\./g, '_');
Expand Down Expand Up @@ -132,17 +134,122 @@ for (const mod of cfg.modules) {
'',
].join('\n');

writeFileSync(outPath, frontmatter + body);
generated += 1;
generatedFiles.push(relative(PROJECT_ROOT, outPath));
pages.push({ mod, safeName, outPath, title, description, body, frontmatter });
log(`${c.green} ✓${c.reset} ${mod} ${c.dim}→ ${relative(PROJECT_ROOT, outPath)}${c.reset}`);
}

log('');
if (generated === 0) {
if (pages.length === 0) {
die('No pages generated. Check the modules list and searchPath in python-autodoc.json.');
}
log(`${c.green}✓${c.reset} Generated ${c.gold}${generated}${c.reset} page${generated === 1 ? '' : 's'} in ${c.cyan}${relative(PROJECT_ROOT, outputDir)}${c.reset}/`);

// ─── Thin-page post-processor ─────────────────────────────────────────
// Two enrichments:
// 1. Package landings (modules with documented children, e.g. `auditkit`
// when `auditkit.config` is also documented) get an auto-generated
// "## Submodules" section listing each child with its description.
// 2. Thin pages (no module-level docstring → almost-empty body) get a
// `:::note` banner explaining the gap so the reader isn't surprised
// by a near-blank page.
// If `cfg.repoUrl` is set, every enriched page also gets a "View source"
// footer link.
log('');
log(`${c.dim}→ post-processing thin pages${c.reset}`);

const documentedSet = new Set(pages.map((p) => p.mod));
const childrenOf = (mod) => pages.filter((p) => p.mod !== mod && p.mod.startsWith(mod + '.'));

let enriched = 0;
let bannered = 0;

for (const page of pages) {
// Count prose-y lines (excludes headings, fences, lists, tables, raw HTML)
const proseLines = page.body.split('\n').filter((l) => {
const t = l.trim();
if (!t) return false;
if (/^#{1,6} /.test(t)) return false;
if (t.startsWith('```')) return false;
if (t.startsWith('|') || /^[-=]{3,}/.test(t)) return false;
if (/^[-*+] /.test(t)) return false;
if (/^<[^>]+>/.test(t)) return false;
return true;
}).length;
const bodyChars = page.body.replace(/\s+/g, '').length;
// "Thin" = barely any body content. Require *both* near-empty char count
// and zero prose lines so we don't badge pages that have a one-line
// docstring (which is sparse but not absent).
const isThin = bodyChars < 150 && proseLines < 1;

const kids = childrenOf(page.mod);
const isPackageLanding = kids.length > 0;

let newBody = page.body;
let touched = false;

if (isThin && !isPackageLanding) {
// Inject sparse-page banner above body. Worded carefully — we know
// the page is short, but we don't *know* whether the source has a
// docstring (pydoc-markdown sometimes drops them) so phrase as a
// suggestion rather than an assertion.
const banner = [
'',
':::note[This page is sparse]',
`The auto-generated reference for \`${page.mod}\` is short. Expanding the source docstring at the top of \`${page.mod.replace(/\./g, '/')}.py\` (a sentence about purpose, when to use it, and a tiny example) would populate this page with real context.`,
':::',
'',
].join('\n');
newBody = banner + newBody;
bannered += 1;
touched = true;
log(`${c.gold} ⚠${c.reset} thin-page banner on ${page.mod}`);
}

if (isPackageLanding) {
// Build a Submodules section linking to siblings via .md refs
// (Astro/Starlight rewrites these to slug URLs at build time).
const lines = ['', '## Submodules', ''];
for (const kid of kids) {
const summary = kid.description && !kid.description.startsWith('API reference for')
? ` — ${kid.description}`
: '';
lines.push(`- [\`${kid.mod}\`](./${kid.safeName}.md)${summary}`);
}
lines.push('');
const submodulesSection = lines.join('\n');

if (isThin) {
// Replace stub body with brief intro + submodules
newBody = `\nTop-level package — see submodules below for the documented API surface.\n${submodulesSection}`;
} else {
// Append to existing body
newBody = newBody.replace(/\s+$/, '') + '\n' + submodulesSection;
}
enriched += 1;
touched = true;
log(`${c.green} ✓${c.reset} added Submodules section to ${page.mod} (${kids.length} child${kids.length === 1 ? '' : 'ren'})`);
}

// Optional "View source" footer if repoUrl is configured
if (touched && cfg.repoUrl) {
const branch = cfg.repoBranch ?? 'main';
const repo = cfg.repoUrl.replace(/\/$/, '');
const sourcePath = page.mod.replace(/\./g, '/');
// Best-effort: link to the package __init__.py for landing pages,
// module .py for leaf modules. Either way the reader gets close.
const target = isPackageLanding
? `${sourcePath}/__init__.py`
: `${sourcePath}.py`;
newBody = newBody.replace(/\s+$/, '') +
`\n\n## See also\n\n- [View source on GitHub](${repo}/blob/${branch}/${target})\n`;
}

writeFileSync(page.outPath, page.frontmatter + newBody);
}

log('');
log(`${c.green}✓${c.reset} Generated ${c.gold}${pages.length}${c.reset} page${pages.length === 1 ? '' : 's'} in ${c.cyan}${relative(PROJECT_ROOT, outputDir)}${c.reset}/`);
if (enriched || bannered) {
log(`${c.dim} ${enriched} package landing${enriched === 1 ? '' : 's'} enriched, ${bannered} thin page${bannered === 1 ? '' : 's'} flagged${c.reset}`);
}
log(`${c.dim} Sidebar wiring (astro.config.mjs):${c.reset}`);
log(`${c.dim} { label: 'API Reference', autogenerate: { directory: 'api' } }${c.reset}`);
log('');
Loading
Loading