fix: restore default scenario craft rails (#3878)

* fix: restore default scenario craft rails

* fix: freeze plugin craft on snapshots

Generated-By: looper 0.9.7 (runner=fixer, agent=opencode)

---------

Co-authored-by: audit <a@b.c>
This commit is contained in:
lefarcen
2026-06-09 01:49:35 +00:00
committed by GitHub
co-authored by audit
parent fbf1f3c392
commit b144df3fba
12 changed files with 209 additions and 5 deletions
+4 -3
View File
@@ -41,6 +41,7 @@ import {
type ConnectorProbe,
} from './connector-gate.js';
import { deriveAutoAtomSurfaces } from './atoms/auto-surfaces.js';
import { getManifestContextCraft } from './context-craft.js';
export class MissingInputError extends Error {
readonly fields: string[];
@@ -158,9 +159,8 @@ export function applyPlugin(input: ApplyInput): ApplyComputed {
if (skillRef) projectMetadata.skillId = skillRef;
const dsId = pickDesignSystemId(manifest, input.activeProjectDesignSystem);
if (dsId) projectMetadata.designSystemId = dsId;
if (Array.isArray(manifest.od?.context?.craft) && manifest.od!.context!.craft!.length > 0) {
projectMetadata.craftRequires = manifest.od!.context!.craft!.slice();
}
const craftRequires = getManifestContextCraft(manifest);
if (craftRequires.length > 0) projectMetadata.craftRequires = craftRequires;
const queryText = resolveLocalizedText(manifest.od?.useCase?.query, input.locale);
@@ -181,6 +181,7 @@ export function applyPlugin(input: ApplyInput): ApplyComputed {
pinnedRef: input.plugin.pinnedRef,
inputs: validated.coerced,
resolvedContext: resolved.context,
craftRequires,
capabilitiesGranted: granted,
capabilitiesRequired: required,
assetsStaged: assets,
+28
View File
@@ -0,0 +1,28 @@
import type { AppliedPluginSnapshot, InstalledPluginRecord, PluginManifest } from '@open-design/contracts';
export function getPluginContextCraft(plugin: InstalledPluginRecord): string[] {
return getManifestContextCraft(plugin.manifest);
}
export function getManifestContextCraft(manifest: PluginManifest): string[] {
return normalizeCraftRequires(manifest.od?.context?.craft);
}
export function getSnapshotContextCraft(snapshot: AppliedPluginSnapshot): string[] {
return normalizeCraftRequires(snapshot.craftRequires);
}
function normalizeCraftRequires(declared: unknown): string[] {
if (!Array.isArray(declared) || declared.length === 0) return [];
const seen = new Set<string>();
const craft: string[] = [];
for (const entry of declared) {
if (typeof entry !== 'string') continue;
const slug = entry.trim();
if (!slug || seen.has(slug)) continue;
seen.add(slug);
craft.push(slug);
}
return craft;
}
+2
View File
@@ -74,6 +74,7 @@ export function migratePlugins(db: SqliteDb): void {
task_kind TEXT NOT NULL,
inputs_json TEXT NOT NULL,
resolved_context_json TEXT NOT NULL,
craft_requires_json TEXT NOT NULL DEFAULT '[]',
pipeline_json TEXT,
genui_surfaces_json TEXT NOT NULL DEFAULT '[]',
capabilities_granted TEXT NOT NULL,
@@ -201,6 +202,7 @@ export function migratePlugins(db: SqliteDb): void {
['resolved_source', `ALTER TABLE applied_plugin_snapshots ADD COLUMN resolved_source TEXT`],
['resolved_ref', `ALTER TABLE applied_plugin_snapshots ADD COLUMN resolved_ref TEXT`],
['archive_integrity', `ALTER TABLE applied_plugin_snapshots ADD COLUMN archive_integrity TEXT`],
['craft_requires_json', `ALTER TABLE applied_plugin_snapshots ADD COLUMN craft_requires_json TEXT NOT NULL DEFAULT '[]'`],
] as const) {
if (!snapshotCols.some((c) => c['name'] === name)) db.exec(ddl);
}
@@ -42,6 +42,7 @@ import {
linkSnapshotToProject,
linkSnapshotToRun,
} from './snapshots.js';
import { getManifestContextCraft } from './context-craft.js';
import {
type ConnectorProbe,
} from './connector-gate.js';
@@ -274,6 +275,7 @@ export function resolvePluginSnapshot(input: ResolveSnapshotInput): ResolveSnaps
taskKind: result.appliedPlugin.taskKind,
inputs: result.appliedPlugin.inputs,
resolvedContext: result.appliedPlugin.resolvedContext,
craftRequires: result.appliedPlugin.craftRequires ?? getManifestContextCraft(plugin.manifest),
pipeline: result.appliedPlugin.pipeline,
genuiSurfaces: result.appliedPlugin.genuiSurfaces ?? [],
capabilitiesGranted: merged,
+6 -2
View File
@@ -51,6 +51,7 @@ export interface CreateSnapshotInput {
taskKind: AppliedPluginSnapshot['taskKind'];
inputs: Record<string, string | number | boolean>;
resolvedContext: ResolvedContext;
craftRequires?: string[] | undefined;
pipeline?: PluginPipeline | undefined;
genuiSurfaces?: GenUISurfaceSpec[] | undefined;
capabilitiesGranted: string[];
@@ -81,13 +82,13 @@ export function createSnapshot(db: SqliteDb, input: CreateSnapshotInput): Applie
manifest_source_digest, source_marketplace_id, source_marketplace_entry_name,
source_marketplace_entry_version, marketplace_trust, resolved_source,
resolved_ref, archive_integrity, pinned_ref, task_kind,
inputs_json, resolved_context_json, pipeline_json, genui_surfaces_json,
inputs_json, resolved_context_json, craft_requires_json, pipeline_json, genui_surfaces_json,
capabilities_granted, capabilities_required, assets_staged_json,
connectors_required_json, connectors_resolved_json, mcp_servers_json,
plugin_title, plugin_description, query_text,
status, applied_at, expires_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'fresh', ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'fresh', ?, ?)
`).run(
id,
input.projectId,
@@ -108,6 +109,7 @@ export function createSnapshot(db: SqliteDb, input: CreateSnapshotInput): Applie
input.taskKind,
JSON.stringify(input.inputs),
JSON.stringify(input.resolvedContext),
JSON.stringify(input.craftRequires ?? []),
input.pipeline ? JSON.stringify(input.pipeline) : null,
JSON.stringify(input.genuiSurfaces ?? []),
JSON.stringify(input.capabilitiesGranted),
@@ -352,6 +354,7 @@ function buildSnapshot(args: {
pinnedRef: input.pinnedRef ?? undefined,
inputs: input.inputs,
resolvedContext: input.resolvedContext,
craftRequires: input.craftRequires ?? [],
capabilitiesGranted: input.capabilitiesGranted,
capabilitiesRequired: input.capabilitiesRequired,
assetsStaged: input.assetsStaged,
@@ -388,6 +391,7 @@ export function rowToSnapshot(row: DbRow): AppliedPluginSnapshot {
pinnedRef: row['pinned_ref'] != null ? String(row['pinned_ref']) : undefined,
inputs: parseJsonOr<Record<string, string | number | boolean>>(row['inputs_json'], {}),
resolvedContext: parseJsonOr<ResolvedContext>(row['resolved_context_json'], { items: [] }),
craftRequires: parseJsonOr<string[]>(row['craft_requires_json'], []),
capabilitiesGranted: parseJsonOr<string[]>(row['capabilities_granted'], []),
capabilitiesRequired: parseJsonOr<string[]>(row['capabilities_required'], []),
assetsStaged: parseJsonOr<PluginAssetRef[]>(row['assets_staged_json'], []),
+4
View File
@@ -10675,6 +10675,10 @@ export async function startServer({
try {
const snap = getSnapshot(db, appliedPluginSnapshotId);
if (snap?.pluginId) {
const { getSnapshotContextCraft } = await import('./plugins/context-craft.js');
for (const craft of getSnapshotContextCraft(snap)) {
if (!skillCraftRequires.includes(craft)) skillCraftRequires.push(craft);
}
const plugin = getInstalledPlugin(db, snap.pluginId);
if (plugin) {
const { loadPluginLocalSkill } = await import('./plugins/local-skill.js');
@@ -88,6 +88,9 @@ describe('plugins/_official/scenarios roster', () => {
const manifestPath = path.join(scenariosRoot, 'od-default', 'open-design.json');
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
expect(manifest.od.hidden).toBe(true);
expect(manifest.od.context?.craft).toEqual(
expect.arrayContaining(['typography', 'color', 'anti-ai-slop']),
);
expect(manifest.od.pipeline.stages[0].id).toBe('task-type');
expect(manifest.od.genui.surfaces).toEqual(
expect.arrayContaining([
@@ -99,4 +102,12 @@ describe('plugins/_official/scenarios roster', () => {
]),
);
});
it('od-new-generation declares the default craft rails for anti-slop HTML output', async () => {
const manifestPath = path.join(scenariosRoot, 'od-new-generation', 'open-design.json');
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
expect(manifest.od.context?.craft).toEqual(
expect.arrayContaining(['typography', 'color', 'anti-ai-slop']),
);
});
});
@@ -0,0 +1,126 @@
import { describe, expect, it } from 'vitest';
import type { AppliedPluginSnapshot, InstalledPluginRecord, PluginManifest } from '@open-design/contracts';
import { getPluginContextCraft, getSnapshotContextCraft } from '../src/plugins/context-craft.js';
function pluginRecord(manifest: PluginManifest): InstalledPluginRecord {
return {
id: 'fixture-plugin',
title: 'Fixture Plugin',
version: '0.1.0',
sourceKind: 'local',
source: '/tmp/fixture-plugin',
sourceMarketplaceId: undefined,
pinnedRef: undefined,
sourceDigest: undefined,
trust: 'trusted',
capabilitiesGranted: ['prompt:inject'],
fsPath: '/tmp/fixture-plugin',
installedAt: 0,
updatedAt: 0,
manifest,
};
}
describe('getPluginContextCraft', () => {
it('returns the declared plugin craft slugs in manifest order', () => {
const manifest: PluginManifest = {
name: 'fixture-plugin',
title: 'Fixture Plugin',
version: '0.1.0',
description: 'Fixture plugin.',
od: {
kind: 'scenario',
taskKind: 'new-generation',
useCase: { query: 'Generate a fixture artifact.' },
context: {
craft: ['typography', 'color', 'anti-ai-slop'],
},
capabilities: ['prompt:inject'],
},
};
expect(getPluginContextCraft(pluginRecord(manifest))).toEqual([
'typography',
'color',
'anti-ai-slop',
]);
});
it('drops blanks, non-strings, and duplicates', () => {
const manifest: PluginManifest = {
name: 'fixture-plugin',
title: 'Fixture Plugin',
version: '0.1.0',
description: 'Fixture plugin.',
od: {
kind: 'scenario',
taskKind: 'new-generation',
useCase: { query: 'Generate a fixture artifact.' },
context: {
craft: [' typography ', '', 'color', 'typography', 42 as never],
},
capabilities: ['prompt:inject'],
},
};
expect(getPluginContextCraft(pluginRecord(manifest))).toEqual([
'typography',
'color',
]);
});
it('returns an empty array when no craft is declared', () => {
const manifest: PluginManifest = {
name: 'fixture-plugin',
title: 'Fixture Plugin',
version: '0.1.0',
description: 'Fixture plugin.',
od: {
kind: 'scenario',
taskKind: 'new-generation',
useCase: { query: 'Generate a fixture artifact.' },
capabilities: ['prompt:inject'],
},
};
expect(getPluginContextCraft(pluginRecord(manifest))).toEqual([]);
});
it('keeps replay craft frozen on the applied snapshot after the installed manifest changes', () => {
const installedManifest: PluginManifest = {
name: 'fixture-plugin',
title: 'Fixture Plugin',
version: '0.1.0',
description: 'Fixture plugin.',
od: {
kind: 'scenario',
taskKind: 'new-generation',
useCase: { query: 'Generate a fixture artifact.' },
context: { craft: ['color'] },
capabilities: ['prompt:inject'],
},
};
const snapshot: AppliedPluginSnapshot = {
snapshotId: 'snapshot-1',
pluginId: 'fixture-plugin',
pluginVersion: '0.1.0',
manifestSourceDigest: 'digest-before-update',
inputs: {},
resolvedContext: { items: [] },
craftRequires: ['typography', 'anti-ai-slop'],
capabilitiesGranted: ['prompt:inject'],
capabilitiesRequired: ['prompt:inject'],
assetsStaged: [],
taskKind: 'new-generation',
appliedAt: 1,
connectorsRequired: [],
connectorsResolved: [],
mcpServers: [],
status: 'fresh',
};
expect(getPluginContextCraft(pluginRecord(installedManifest))).toEqual(['color']);
expect(getSnapshotContextCraft(snapshot)).toEqual(['typography', 'anti-ai-slop']);
});
});
@@ -95,6 +95,17 @@ describe('snapshots writer', () => {
expect(fetched!.status).toBe('fresh');
});
it('persists apply-time craft requirements on the immutable snapshot', () => {
db.prepare('INSERT INTO projects (id, name) VALUES (?, ?)').run('project-1', 'Project 1');
const snap = createSnapshot(db, baseInput({
craftRequires: ['typography', 'color', 'anti-ai-slop'],
}));
const fetched = getSnapshot(db, snap.snapshotId);
expect(fetched?.craftRequires).toEqual(['typography', 'color', 'anti-ai-slop']);
});
it('linkSnapshotToRun pins expires_at to NULL', () => {
db.prepare('INSERT INTO projects (id, name) VALUES (?, ?)').run('project-1', 'Project 1');
const snap = createSnapshot(db, baseInput());
+1
View File
@@ -54,6 +54,7 @@ export const AppliedPluginSnapshotSchema = z.object({
pinnedRef: z.string().optional(),
inputs: z.record(z.union([z.string(), z.number(), z.boolean()])),
resolvedContext: ResolvedContextSchema,
craftRequires: z.array(z.string()).optional(),
capabilitiesGranted: z.array(z.string()),
capabilitiesRequired: z.array(z.string()),
assetsStaged: z.array(PluginAssetRefSchema),
@@ -71,6 +71,13 @@
"scenario": "default-router",
"mode": "scenario",
"hidden": true,
"context": {
"craft": [
"typography",
"color",
"anti-ai-slop"
]
},
"pipeline": {
"stages": [
{
@@ -68,6 +68,13 @@
"taskKind": "new-generation",
"scenario": "new-generation",
"mode": "scenario",
"context": {
"craft": [
"typography",
"color",
"anti-ai-slop"
]
},
"pipeline": {
"stages": [
{