mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-19 01:46:15 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The feature map describes its user-facing capabilities and verification recipes. > - Storybook provides isolated UI examples for those capabilities. > - The previous organization showed only features with primary story suites. > - This pull request derives the complete Storybook index from the upstream map. > - Readers can find the same features in both places and see related examples or explicit gaps. ## Linked Issues or Issue Description Refs #16046 and #16049. **What existing behavior does this improve?** Storybook navigation and the downstream feature catalog. **Current behavior** The sidebar has 28 primary feature homes. The feature map lists 35. The main guide groups suites alphabetically by area slug. Related examples can live under a different feature. **Proposed behavior** Use the feature map's eight sections and 35 features in the same order. Give each feature an Overview page with all its capabilities, related example links, and missing-example messages. Keep shared foundations outside the product index. Preserve all existing stories and URLs. **Reason and benefit** Readers can move between the feature map and Storybook without learning a second index. A feature without cataloged examples remains visible. **Breaking changes** None to existing story IDs or product behavior. Storybook start and build now reject stale generated feature pages and ordering. Run the catalog generator after upstream changes, inspect the output, and review affected source fingerprints. ## What Changed - Generate 35 feature Overview stories and capability data from the upstream map. Generate the inline sidebar order required by Storybook. - Replace the main guide with the full ordered feature index and search. Link all related suites from feature pages, even across primary homes. - Preserve all 223 authored files, 2,210 story IDs, and the existing docs ID. Keep status, implementation, and fixture classifications visible. - Put the generated Markdown catalog in feature-map order. Keep generated guides outside example counts. - Update maintenance and review skills, documentation, and generator tests. Keep the feature map independent of Storybook. ## Verification - Passed: 16 catalog and generator tests; map checks; token gates; both skill validators. - Passed: `pnpm -r typecheck`, `pnpm build`, and the Storybook build. - Built-index comparison preserved all 2,211 previous entries and added only 35 feature overviews. Feature order matches the map. - Browser: inspected the complete index and sidebar, searched for Goals, opened its overview, and followed a related suite under Projects. The original suite renders. - Passed: all 8,170 UI tests in 728 files. Browser checks also covered empty search, capability-ID search, missing-example pages, the original editable-text bookmark, the CLI sidebar label, and searching/opening shared foundations. - The broad local `pnpm test:run` was stopped after a failure in the unchanged GitHub chat test `authorizes description edits as the editor, never inheriting the original author's access`. That exact test passed alone without code or expectation changes. A complete local repository suite pass is not established. - Original revision CI passed after one runner retry. The merged revision `5ca01c70716f23974680a8019ddc613035d11bc4` passed fresh CI. Local UI typecheck, catalog checks, 16 generator tests, map checks and token gates pass. - Greptile: 5/5 on final commit `5ca01c70716f23974680a8019ddc613035d11bc4`, with all findings resolved. - Master merge: inspected source drift in 61 catalog entries and regenerated INDEX, preserving upstream notes. Marked the sidebar suite that mounts IssueDetail unreviewed for the new activity query; recorded limits for composer and channel-default changes. No stories removed. - Final Storybook build passed. Browser filename searches for `new-task-composer` and `forms-editors` return the expected feature and shared example. - No visual-baseline or full production acceptance claim. The deeper authored scenario groups remain intact. ## Risks - The parser depends on the feature-map Markdown contract. It rejects unknown index rows, omitted feature recipes, and unexpected generated files. - A suite can reference several capabilities. A link proves catalog association, not complete behavior coverage or production parity. - Storybook treats `/` as a folder separator. Sidebar labels use a comma for that character; the guide and page headings retain the upstream name. - No database, API, authorization, or production component changes. ## Model Used OpenAI Codex, GPT-6 family, with reasoning, code execution, and browser inspection. The exact runtime model ID and context window are not exposed in this session. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
127 lines
5.8 KiB
JavaScript
127 lines
5.8 KiB
JavaScript
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
export const generatedRoot = "ui/storybook/generated";
|
|
export const sidebarLabel = title => title.replaceAll("/", ", ");
|
|
|
|
export function featureIndex(root) {
|
|
const sections = [];
|
|
let section;
|
|
const seen = new Set();
|
|
const source = readFileSync(path.join(root, "maps/feature-map/README.md"), "utf8");
|
|
for (const line of source.split("\n")) {
|
|
if (line.startsWith("### ")) {
|
|
section = { title: line.slice(4), features: [] };
|
|
sections.push(section);
|
|
}
|
|
const match = line.match(/^\| \[([^\]]+)\]\(\.\/([a-z0-9-]+)\.md\) \| (.*) \|$/);
|
|
if (!match) {
|
|
if (line.startsWith("| [")) throw new Error(`Unrecognized feature-map row: ${line}`);
|
|
continue;
|
|
}
|
|
if (!section || seen.has(match[2])) throw new Error(`Invalid feature-map entry: ${line}`);
|
|
const [, title, id, description] = match;
|
|
seen.add(id);
|
|
const recipe = readFileSync(path.join(root, `maps/feature-map/${id}.md`), "utf8");
|
|
const body = recipe.split(/^## Sub-features\s*$/m)[1]?.split(/^## /m)[0];
|
|
if (!body) throw new Error(`Missing sub-features: ${id}`);
|
|
const capabilities = [];
|
|
for (const row of body.trim().split("\n")) {
|
|
const capability = row.match(/^- `([^`]+)`:\s*(.*)$/);
|
|
if (capability) {
|
|
const [, key, text] = capability;
|
|
capabilities.push({ id: `${id}/${key}`, label: key.replaceAll("-", " ").replace(/^./, letter => letter.toUpperCase()), description: text });
|
|
} else if (row.trim() && capabilities.length) {
|
|
capabilities.at(-1).description += ` ${row.trim()}`;
|
|
}
|
|
}
|
|
if (!capabilities.length || new Set(capabilities.map(item => item.id)).size !== capabilities.length) throw new Error(`Invalid sub-features: ${id}`);
|
|
section.features.push({ id, title, description, capabilities });
|
|
}
|
|
const populated = sections.filter(item => item.features.length);
|
|
if (!populated.length) throw new Error("Feature-map index has no feature families");
|
|
for (const file of readdirSync(path.join(root, "maps/feature-map"))) {
|
|
if (file.endsWith(".md") && file !== "README.md" && !seen.has(file.slice(0, -3))) throw new Error(`Feature recipe missing from index: ${file}`);
|
|
}
|
|
return { sections: populated };
|
|
}
|
|
|
|
export function featureOrder(index) {
|
|
return ["Overview", ...index.sections.flatMap(section => [sidebarLabel(section.title),
|
|
section.features.flatMap(feature => [sidebarLabel(feature.title), ["Overview", "*"]]),
|
|
]), "Foundations"];
|
|
}
|
|
|
|
export function featureArtifacts(index) {
|
|
const outputs = new Map([
|
|
[`${generatedRoot}/feature-index.json`, JSON.stringify(index, null, 2) + "\n"],
|
|
]);
|
|
for (const section of index.sections) {
|
|
for (const feature of section.features) {
|
|
outputs.set(`${generatedRoot}/features/${feature.id}.stories.tsx`, [
|
|
'import type { Meta, StoryObj } from "@storybook/react-vite";',
|
|
'import { FeatureOverview } from "../../support/feature-map/FeatureOverview";',
|
|
"",
|
|
"const meta = {",
|
|
` id: ${JSON.stringify(`feature-map-${feature.id}`)},`,
|
|
` title: ${JSON.stringify(`${sidebarLabel(section.title)}/${sidebarLabel(feature.title)}/Overview`)},`,
|
|
" component: FeatureOverview,",
|
|
` args: { area: ${JSON.stringify(feature.id)} },`,
|
|
' tags: ["!autodocs"],',
|
|
' parameters: { layout: "fullscreen" },',
|
|
"} satisfies Meta<typeof FeatureOverview>;",
|
|
"export default meta;",
|
|
"export const Overview: StoryObj<typeof meta> = {};",
|
|
"",
|
|
].join("\n"));
|
|
}
|
|
}
|
|
return outputs;
|
|
}
|
|
|
|
export function reconcileFeatureArtifacts(root, index, write = false) {
|
|
const outputs = featureArtifacts(index);
|
|
const errors = [];
|
|
function inspect(directory) {
|
|
if (!existsSync(path.join(root, directory))) return;
|
|
for (const entry of readdirSync(path.join(root, directory), { withFileTypes: true })) {
|
|
const file = `${directory}/${entry.name}`;
|
|
if (entry.isDirectory()) inspect(file);
|
|
else if (!outputs.has(file)) errors.push(`Unexpected generated feature file: ${file}`);
|
|
}
|
|
}
|
|
inspect(generatedRoot);
|
|
if (errors.length) return errors;
|
|
for (const [file, content] of outputs) {
|
|
const destination = path.join(root, file);
|
|
if (write) {
|
|
mkdirSync(path.dirname(destination), { recursive: true });
|
|
writeFileSync(destination, content);
|
|
} else if (!existsSync(destination) || readFileSync(destination, "utf8") !== content) {
|
|
errors.push(`${file}: generated feature index is stale; run storybook-catalog.mjs --write`);
|
|
}
|
|
}
|
|
const previewFile = "ui/storybook/.storybook/preview.tsx";
|
|
const previewPath = path.join(root, previewFile);
|
|
const preview = readFileSync(previewPath, "utf8");
|
|
const pattern = /(\bstorySort:\s*\{\s*order:\s*)([\s\S]*?)(,\s*\n\s*\},)/g;
|
|
if ([...preview.matchAll(pattern)].length !== 1) throw new Error("Expected one inline storySort.order in preview.tsx");
|
|
const order = JSON.stringify(featureOrder(index), null, 2)
|
|
.replace(/("[^"]+",)\n\s+\[\n\s+"Overview",\n\s+"\*"\n\s+\]/g, '$1 ["Overview", "*"]')
|
|
.replaceAll("\n", "\n ");
|
|
const updated = preview.replace(pattern, (_, before, value, after) => before + order + after);
|
|
if (write) writeFileSync(previewPath, updated);
|
|
else if (updated !== preview) errors.push(`${previewFile}: generated sidebar order is stale; run storybook-catalog.mjs --write`);
|
|
return errors;
|
|
}
|
|
|
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
const root = fileURLToPath(new URL("..", import.meta.url));
|
|
const errors = reconcileFeatureArtifacts(root, featureIndex(root));
|
|
if (errors.length) {
|
|
errors.forEach(error => console.error(error));
|
|
process.exitCode = 1;
|
|
}
|
|
}
|