mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-14 23:26:39 +03:00
Add concept world catalog and review workflow
AI-assisted: prepared by Codex at Paul's request.
This commit is contained in:
+42
-4
@@ -440,6 +440,38 @@ function copyDirSync(src, dest) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an existing directory match a generated source without removing the
|
||||
* destination root. Provider skill roots can be watched by the running agent;
|
||||
* replacing that root fails on some platforms and can invalidate the live
|
||||
* skill path mid-session.
|
||||
*/
|
||||
function mirrorDirContentsSync(src, dest) {
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
const sourceEntries = new Map(
|
||||
fs.readdirSync(src, { withFileTypes: true }).map(entry => [entry.name, entry]),
|
||||
);
|
||||
|
||||
for (const destEntry of fs.readdirSync(dest, { withFileTypes: true })) {
|
||||
if (!sourceEntries.has(destEntry.name)) {
|
||||
fs.rmSync(path.join(dest, destEntry.name), { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
for (const entry of sourceEntries.values()) {
|
||||
const srcPath = path.join(src, entry.name);
|
||||
const destPath = path.join(dest, entry.name);
|
||||
const destStat = fs.existsSync(destPath) ? fs.lstatSync(destPath) : null;
|
||||
if (entry.isDirectory()) {
|
||||
if (destStat && !destStat.isDirectory()) fs.rmSync(destPath, { recursive: true, force: true });
|
||||
mirrorDirContentsSync(srcPath, destPath);
|
||||
} else {
|
||||
if (destStat?.isDirectory()) fs.rmSync(destPath, { recursive: true, force: true });
|
||||
fs.copyFileSync(srcPath, destPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function syncRootHookManifests(rootDir) {
|
||||
const synced = [];
|
||||
for (const config of Object.values(PROVIDERS)) {
|
||||
@@ -688,11 +720,17 @@ async function build() {
|
||||
|
||||
if (fs.existsSync(skillsSrc)) {
|
||||
// Preserve legacy per-project script artifacts (e.g. live-mode config.json)
|
||||
// across the rm + recopy. The build intentionally doesn't ship them,
|
||||
// so without this the sync destroys local state on every rebuild.
|
||||
// while replacing only skills generated by this build. Removing the
|
||||
// whole provider skills directory can erase unrelated repo-local skills,
|
||||
// and watched directories such as `.agents/skills` may reject the parent
|
||||
// removal while Codex is using them.
|
||||
const stashed = stashPerProjectArtifacts(skillsDest);
|
||||
if (fs.existsSync(skillsDest)) fs.rmSync(skillsDest, { recursive: true });
|
||||
copyDirSync(skillsSrc, skillsDest);
|
||||
fs.mkdirSync(skillsDest, { recursive: true });
|
||||
for (const entry of fs.readdirSync(skillsSrc, { withFileTypes: true })) {
|
||||
const generatedDest = path.join(skillsDest, entry.name);
|
||||
if (entry.isDirectory()) mirrorDirContentsSync(path.join(skillsSrc, entry.name), generatedDest);
|
||||
else fs.copyFileSync(path.join(skillsSrc, entry.name), generatedDest);
|
||||
}
|
||||
restorePerProjectArtifacts(skillsDest, stashed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,7 +263,7 @@ export function createTransformer(config) {
|
||||
const scriptsOutDir = path.join(skillDir, 'scripts');
|
||||
ensureDir(scriptsOutDir);
|
||||
for (const script of skill.scripts) {
|
||||
const scriptContent = replaceScriptProviderMarker(script.content, placeholderKey);
|
||||
const scriptContent = replaceScriptProviderMarker(script.content, placeholderKey, provider);
|
||||
writeFile(path.join(scriptsOutDir, script.name), scriptContent);
|
||||
scriptCount++;
|
||||
}
|
||||
|
||||
@@ -763,12 +763,14 @@ export function replacePlaceholders(content, provider, commandNames = [], allSki
|
||||
* their command prefix from lib/provider.mjs, whose declaration is replaced
|
||||
* here by an exact string match.
|
||||
*/
|
||||
export function replaceScriptProviderMarker(content, provider) {
|
||||
export function replaceScriptProviderMarker(content, provider, buildProvider = provider) {
|
||||
const placeholders = PROVIDER_PLACEHOLDERS[provider] || PROVIDER_PLACEHOLDERS.cursor;
|
||||
const commandPrefix = placeholders.command_prefix || '/';
|
||||
const marker = "export const IMPECCABLE_COMMAND_PREFIX = '/'; // @impeccable-provider-command-prefix";
|
||||
const rendered = `export const IMPECCABLE_COMMAND_PREFIX = ${JSON.stringify(commandPrefix)};`;
|
||||
return content.replace(marker, rendered);
|
||||
const prefixMarker = "export const IMPECCABLE_COMMAND_PREFIX = '/'; // @impeccable-provider-command-prefix";
|
||||
const providerMarker = "export const IMPECCABLE_PROVIDER_ID = 'source'; // @impeccable-provider-id";
|
||||
return content
|
||||
.replace(prefixMarker, `export const IMPECCABLE_COMMAND_PREFIX = ${JSON.stringify(commandPrefix)};`)
|
||||
.replace(providerMarker, `export const IMPECCABLE_PROVIDER_ID = ${JSON.stringify(buildProvider)};`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,11 +25,11 @@ export const SUITES = {
|
||||
triggers: [
|
||||
...COMMON_INFRA_PATTERNS,
|
||||
/^scripts\/(?!benchmark-detector|build-browser-detector|build-extension)/,
|
||||
/^skill\/(SKILL\.src\.md|agents\/|reference\/|scripts\/(cleanup-deprecated|concept-seed|context|context-signals|critique-storage|design-parser|hook|impeccable-paths|is-generated|lib\/(provider|surface-briefs|target-slug)|pin|surface-brief))/,
|
||||
/^skill\/(SKILL\.src\.md|agents\/|reference\/|scripts\/(cleanup-deprecated|concept-ingredients|concept-reviews|concept-seed|context|context-signals|critique-storage|design-parser|hook|impeccable-paths|is-generated|lib\/(concept-catalog|provider|surface-briefs|target-slug)|pin|surface-brief|validate-concept-catalog))/,
|
||||
/^site\/(pages|content|components|layouts)\//,
|
||||
/^README(\.npm)?\.md$/,
|
||||
/^cli\/bin\//,
|
||||
/^tests\/(build|cleanup-deprecated|cli-ignores|concept-seed|context|context-signals|critique-storage|design-parser|docs-integrity|github-sheriff|hook|hook-build|impeccable-paths|openai-plugin|pin|shiki-theme|skills-cli|slop-catalog|surface-brief|target-args|test-suites|theme|windows-path-fix|zip)\.test\.(js|mjs)$/,
|
||||
/^tests\/(build|cleanup-deprecated|cli-ignores|concept-seed|context|context-signals|critique-storage|design-parser|docs-integrity|github-sheriff|hook|hook-build|impeccable-paths|openai-plugin|pin|shiki-theme|skills-cli|slop-catalog|surface-brief|target-args|test-suites|theme|windows-path-fix|worlds-review-vite-plugin|zip)\.test\.(js|mjs)$/,
|
||||
/^tests\/lib\//,
|
||||
],
|
||||
commands: [
|
||||
@@ -71,6 +71,7 @@ export const SUITES = {
|
||||
'tests/surface-brief.test.mjs',
|
||||
'tests/test-suites.test.mjs',
|
||||
'tests/theme.test.mjs',
|
||||
'tests/worlds-review-vite-plugin.test.mjs',
|
||||
'tests/zip.test.mjs',
|
||||
],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import { readFile, rename, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
const API_PATH = '/__impeccable/worlds';
|
||||
const MAX_BODY_BYTES = 64 * 1024;
|
||||
const REVIEW_STATUSES = new Set(['pending', 'approved', 'rejected']);
|
||||
|
||||
function jsonResponse(res, status, payload) {
|
||||
res.statusCode = status;
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
res.setHeader('Cache-Control', 'no-store');
|
||||
res.end(`${JSON.stringify(payload)}\n`);
|
||||
}
|
||||
|
||||
function sameOrigin(req) {
|
||||
const origin = req.headers.origin;
|
||||
if (!origin) return true;
|
||||
try {
|
||||
return new URL(origin).host === req.headers.host;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function readJsonBody(req) {
|
||||
const chunks = [];
|
||||
let size = 0;
|
||||
for await (const chunk of req) {
|
||||
size += chunk.length;
|
||||
if (size > MAX_BODY_BYTES) throw new Error('Request body is too large');
|
||||
chunks.push(chunk);
|
||||
}
|
||||
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
||||
}
|
||||
|
||||
async function readJson(filePath) {
|
||||
return JSON.parse(await readFile(filePath, 'utf8'));
|
||||
}
|
||||
|
||||
async function writeJsonAtomic(filePath, value) {
|
||||
const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
||||
await writeFile(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
||||
await rename(temporaryPath, filePath);
|
||||
}
|
||||
|
||||
function findConcept(catalog, id) {
|
||||
for (const family of catalog.families || []) {
|
||||
const index = family.concepts?.findIndex(concept => concept.id === id) ?? -1;
|
||||
if (index !== -1) return { family, index, concept: family.concepts[index] };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateTags(tags) {
|
||||
return Array.isArray(tags)
|
||||
&& tags.length === 3
|
||||
&& tags.every(tag => typeof tag === 'string' && tag.trim().length >= 2 && tag.trim().length <= 40);
|
||||
}
|
||||
|
||||
export function worldsReviewPlugin({ root = process.cwd() } = {}) {
|
||||
const catalogPath = path.join(root, 'skill', 'scripts', 'concept-ingredients.json');
|
||||
const reviewsPath = path.join(root, 'skill', 'scripts', 'concept-reviews.json');
|
||||
let mutationQueue = Promise.resolve();
|
||||
|
||||
async function mutate(body) {
|
||||
const catalog = await readJson(catalogPath);
|
||||
const reviewData = await readJson(reviewsPath);
|
||||
const match = findConcept(catalog, body.id);
|
||||
if (!match) throw new Error('Concept was not found');
|
||||
|
||||
if (body.action === 'review') {
|
||||
if (!REVIEW_STATUSES.has(body.status)) throw new Error('Review status is invalid');
|
||||
if (body.status === 'pending') {
|
||||
delete reviewData.reviews[body.id];
|
||||
} else {
|
||||
reviewData.reviews[body.id] = {
|
||||
status: body.status,
|
||||
reviewedBy: 'pbakaus',
|
||||
reviewedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
reviewData.reviews = Object.fromEntries(Object.entries(reviewData.reviews).sort(([a], [b]) => a.localeCompare(b)));
|
||||
await writeJsonAtomic(reviewsPath, reviewData);
|
||||
return { id: body.id, status: body.status, review: reviewData.reviews[body.id] || null };
|
||||
}
|
||||
|
||||
if (body.action === 'update') {
|
||||
const form = typeof body.form === 'string' ? body.form.trim() : '';
|
||||
const lineage = typeof body.lineage === 'string' ? body.lineage.trim() : '';
|
||||
const targetFamily = catalog.families.find(family => family.id === body.familyId);
|
||||
if (form.length < 12 || form.length > 600 || !form.includes(',')) {
|
||||
throw new Error('Form must be 12–600 characters and include inherited structure after a comma');
|
||||
}
|
||||
if (lineage.length < 2 || lineage.length > 160) throw new Error('Lineage must be 2–160 characters');
|
||||
if (!validateTags(body.tags)) throw new Error('Exactly three structural tags are required');
|
||||
if (!targetFamily) throw new Error('Family was not found');
|
||||
|
||||
const updated = {
|
||||
...match.concept,
|
||||
form,
|
||||
lineage,
|
||||
tags: body.tags.map(tag => tag.trim()),
|
||||
};
|
||||
if (targetFamily.id === match.family.id) {
|
||||
match.family.concepts[match.index] = updated;
|
||||
} else {
|
||||
match.family.concepts.splice(match.index, 1);
|
||||
targetFamily.concepts.push(updated);
|
||||
targetFamily.concepts.sort((a, b) => a.id.localeCompare(b.id));
|
||||
}
|
||||
catalog.catalogVersion = new Date().toISOString();
|
||||
await writeJsonAtomic(catalogPath, catalog);
|
||||
return { id: body.id, concept: updated, familyId: targetFamily.id };
|
||||
}
|
||||
|
||||
throw new Error('Action is invalid');
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'impeccable-worlds-review',
|
||||
apply: 'serve',
|
||||
configureServer(server) {
|
||||
server.middlewares.use(API_PATH, async (req, res) => {
|
||||
if (req.method !== 'POST') {
|
||||
jsonResponse(res, 405, { error: 'Method not allowed' });
|
||||
return;
|
||||
}
|
||||
if (!sameOrigin(req)) {
|
||||
jsonResponse(res, 403, { error: 'Cross-origin writes are not allowed' });
|
||||
return;
|
||||
}
|
||||
if (!String(req.headers['content-type'] || '').startsWith('application/json')) {
|
||||
jsonResponse(res, 415, { error: 'Expected application/json' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await readJsonBody(req);
|
||||
const operation = mutationQueue.then(() => mutate(body));
|
||||
mutationQueue = operation.catch(() => {});
|
||||
jsonResponse(res, 200, { ok: true, result: await operation });
|
||||
} catch (error) {
|
||||
jsonResponse(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user