@call-me-sensei/toonlab 0.4.19 → 0.4.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NPM-LIBRARY.md +230 -0
- package/README.md +78 -36
- package/agents/PROMPTS.md +91 -0
- package/agents/README.md +9 -14
- package/agents/claude/CLAUDE.md +8 -0
- package/agents/codex/AGENTS.md +161 -0
- package/agents/cursor/toonlab.mdc +37 -0
- package/agents/references/mcp-asset-discovery.md +12 -9
- package/agents/references/runtime-entry-points.md +5 -4
- package/agents/skills/claude/asset-sourcing/SKILL.md +9 -6
- package/agents/skills/claude/rock-ground-shaders/SKILL.md +16 -0
- package/agents/skills/claude/vegetation-sky/SKILL.md +7 -4
- package/agents/skills/codex/asset-sourcing/SKILL.md +9 -6
- package/agents/skills/codex/rock-ground-shaders/SKILL.md +16 -0
- package/agents/skills/codex/vegetation-sky/SKILL.md +7 -4
- package/cli/toonlab.mjs +3 -2
- package/database/fal-patina.mjs +131 -0
- package/database/generation-service.mjs +108 -7
- package/database/migrations/0007_creation_revisions.sql +80 -0
- package/database/migrations/0008_creation_revision_hardening.sql +98 -0
- package/database/migrations/0009_creation_revision_delete_integrity.sql +11 -0
- package/database/migrations/0010_external_catalog_assets.sql +28 -0
- package/database/providers.mjs +195 -16
- package/database/repository.mjs +518 -70
- package/database/seeds/catalog/0003_2026-08-open-assets.sql +159086 -0
- package/database/seeds/catalog/0004_2026-08-c7-rocks.sql +39405 -0
- package/mcp/public-catalog.mjs +4 -52
- package/mcp/server.mjs +175 -12
- package/mcp/vite-plugin.mjs +78 -2
- package/package.json +32 -12
- package/scripts/generate-catalog-seed.mjs +34 -7
- package/src/asset-policy/catalogLicenses.js +64 -6
- package/src/assetlib/loadImported.js +15 -1
- package/src/catalog/officialCatalogAssetRuntime.js +13 -2
- package/src/catalog/officialCatalogLod.js +34 -1
- package/src/catalog/officialCatalogPlacement.js +29 -14
- package/src/character/animationRetarget.js +2 -0
- package/src/character/characterRig.js +6 -1
- package/src/character/characterRuntime.js +175 -7
- package/src/environment/environmentMaterialAdapter.js +30 -0
- package/src/environment/environmentPresets.js +90 -0
- package/src/environment/environmentSunShadowPass.js +42 -0
- package/src/environment/toonLabSurfaceLighting.js +108 -10
- package/src/environment/urbanPropMaterial.js +104 -2
- package/src/lighting/lightingSystem.js +65 -10
- package/src/renderer/index.js +7 -0
- package/src/renderer/styleComparison.js +985 -0
- package/src/rock-shader/index.js +5 -0
- package/src/rock-shader/rockGeometryDetail.js +701 -0
- package/src/rock-shader/rockMaterial.js +782 -39
- package/src/rock-shader/rockRegionRuntime.js +185 -0
- package/src/rock-shader/rockSemanticMaterialRuntime.js +337 -0
- package/src/rock-shader/rockShaderRuntime.js +354 -67
- package/src/rock-shader/rockShaderSettings.js +246 -16
- package/src/rock-shader/rockTangentIntegrity.js +136 -0
- package/src/rock-shader/rockTextureIntegrity.js +115 -0
- package/src/rockgen/index.js +1 -0
- package/src/rockgen/rockDocument.js +27 -3
- package/src/rockgen/surface/c7GeologySurface.js +372 -0
- package/src/shaders-tsl/water.js +8 -0
- package/src/sky/cloudShadow.js +25 -1
- package/src/sky/skySystem.js +9 -0
- package/src/styles/index.js +23 -0
- package/src/styles/neutralStylePresets.js +712 -0
- package/src/styles/sceneStyleRuntime.js +51 -4
- package/src/styles/styleAdapters.js +13 -2
- package/src/styles/styleBundle.js +1 -1
- package/src/texgen/evaluateTexture.js +30 -3
- package/src/texgen/textureGenerators.js +63 -31
- package/src/texgen/textureSettings.js +25 -8
- package/src/toon/toonSettings.js +96 -2
- package/src/vegetation/branchTree.js +327 -42
- package/src/vegetation/index.js +24 -2
- package/src/vegetation/scatter.js +372 -1
- package/src/vegetation/stylizedTree.js +312 -126
- package/src/vegetation/stylizedTreeFoliage.js +229 -7
- package/src/vegetation/treeSurfaceTextures.js +328 -0
- package/src/version.js +1 -1
- package/src/water/waterSettings.js +15 -7
- package/types/asset-policy/catalogLicenses.d.ts +2 -0
- package/types/asset-policy/index.d.ts +2 -0
- package/types/assetlib/loadImported.d.ts +3 -1
- package/types/catalog/officialCatalogLod.d.ts +63 -5
- package/types/character/characterRig.d.ts +59 -0
- package/types/character/characterRuntime.d.ts +1 -0
- package/types/environment/environmentMaterialAdapter.d.ts +3 -1
- package/types/environment/toonLabSurfaceLighting.d.ts +20 -3
- package/types/index.d.ts +89 -7
- package/types/lighting/lightingSystem.d.ts +7 -0
- package/types/react/index.d.ts +20 -0
- package/types/renderer/index.d.ts +1 -0
- package/types/renderer/styleComparison.d.ts +409 -0
- package/types/rock-shader/index.d.ts +5 -0
- package/types/rock-shader/rockGeometryDetail.d.ts +300 -0
- package/types/rock-shader/rockMaterial.d.ts +3 -0
- package/types/rock-shader/rockRegionRuntime.d.ts +86 -0
- package/types/rock-shader/rockSemanticMaterialRuntime.d.ts +29 -0
- package/types/rock-shader/rockShaderRuntime.d.ts +2 -0
- package/types/rock-shader/rockShaderSettings.d.ts +19 -5
- package/types/rock-shader/rockTangentIntegrity.d.ts +51 -0
- package/types/rock-shader/rockTextureIntegrity.d.ts +61 -0
- package/types/rockgen/index.d.ts +1 -0
- package/types/rockgen/rockDocument.d.ts +29 -1
- package/types/rockgen/surface/c7GeologySurface.d.ts +8 -0
- package/types/sky/cloudShadow.d.ts +1 -0
- package/types/styles/index.d.ts +1 -0
- package/types/styles/neutralStylePresets.d.ts +944 -0
- package/types/styles/sceneStyleRuntime.d.ts +20 -0
- package/types/vegetation/branchTree.d.ts +4 -0
- package/types/vegetation/index.d.ts +9 -7
- package/types/vegetation/scatter.d.ts +1 -0
- package/types/vegetation/stylizedTree.d.ts +1 -2
- package/types/vegetation/stylizedTreeFoliage.d.ts +1 -0
- package/types/version.d.ts +1 -1
- package/agents/references/geology-playbook.md +0 -118
- package/agents/skills/claude/karst-cliff-construction/SKILL.md +0 -258
- package/agents/skills/claude/outdoor-world/SKILL.md +0 -492
- package/agents/skills/claude/outdoor-world/references/coastal-landform-formation.md +0 -100
- package/agents/skills/codex/karst-cliff-construction/SKILL.md +0 -258
- package/agents/skills/codex/outdoor-world/SKILL.md +0 -492
- package/agents/skills/codex/outdoor-world/references/coastal-landform-formation.md +0 -100
- package/src/vegetation/legacyTreePresets.js +0 -120
- package/types/vegetation/legacyTreePresets.d.ts +0 -22
|
@@ -15,10 +15,12 @@ import {
|
|
|
15
15
|
pollTripoTask,
|
|
16
16
|
runProvider,
|
|
17
17
|
} from './providers.mjs';
|
|
18
|
+
import { PATINA_MAP_TYPES } from './fal-patina.mjs';
|
|
18
19
|
|
|
19
20
|
const IMAGE_KINDS = new Set(['image', 'texture_image', 'concept_image']);
|
|
20
21
|
const MODEL_KINDS = new Set(['text_to_model', 'image_to_model', 'multiview_to_model', 'model_segment']);
|
|
21
|
-
const
|
|
22
|
+
const MATERIAL_KINDS = new Set(['material']);
|
|
23
|
+
const RESOLUTIONS = new Set(['1k', '2k', '4k', '8k']);
|
|
22
24
|
const ASPECT_RATIOS = new Set(['1:1', '16:9', '9:16', '4:3', '3:4']);
|
|
23
25
|
|
|
24
26
|
function publicFile(file) {
|
|
@@ -42,6 +44,17 @@ export function publicGenerationJob(job) {
|
|
|
42
44
|
...(job.result.file ? { file: publicFile(job.result.file) } : {}),
|
|
43
45
|
...(job.result.previewFile ? { previewFile: publicFile(job.result.previewFile) } : {}),
|
|
44
46
|
...(job.result.conceptFile ? { conceptFile: publicFile(job.result.conceptFile) } : {}),
|
|
47
|
+
...(job.result.materialBundle ? {
|
|
48
|
+
materialBundle: {
|
|
49
|
+
...job.result.materialBundle,
|
|
50
|
+
...(job.result.materialBundle.manifestFile ? { manifestFile: publicFile(job.result.materialBundle.manifestFile) } : {}),
|
|
51
|
+
variants: (job.result.materialBundle.variants ?? []).map((variant) => ({
|
|
52
|
+
...variant,
|
|
53
|
+
preview: publicFile(variant.preview),
|
|
54
|
+
maps: Object.fromEntries(Object.entries(variant.maps ?? {}).map(([key, file]) => [key, publicFile(file)])),
|
|
55
|
+
})),
|
|
56
|
+
},
|
|
57
|
+
} : {}),
|
|
45
58
|
}
|
|
46
59
|
: job.result;
|
|
47
60
|
return {
|
|
@@ -58,11 +71,13 @@ function configuredImageModel(id, configuration) {
|
|
|
58
71
|
/** Pure request planner shared by MCP validation and regression tests. */
|
|
59
72
|
export function resolveGenerationPlan(args, configuration = providerConfiguration()) {
|
|
60
73
|
const kind = String(args.kind ?? '');
|
|
61
|
-
if (!IMAGE_KINDS.has(kind) && !MODEL_KINDS.has(kind)) {
|
|
74
|
+
if (!IMAGE_KINDS.has(kind) && !MODEL_KINDS.has(kind) && !MATERIAL_KINDS.has(kind)) {
|
|
62
75
|
throw new Error(`Unsupported generation kind "${kind}".`);
|
|
63
76
|
}
|
|
64
77
|
const resolution = String(args.resolution ?? '1k').toLowerCase();
|
|
65
|
-
if (!RESOLUTIONS.has(resolution)
|
|
78
|
+
if (!RESOLUTIONS.has(resolution) || (resolution === '8k' && !MATERIAL_KINDS.has(kind))) {
|
|
79
|
+
throw new Error(`Unsupported resolution "${resolution}".`);
|
|
80
|
+
}
|
|
66
81
|
const aspectRatio = String(args.aspect_ratio ?? '1:1');
|
|
67
82
|
if (!ASPECT_RATIOS.has(aspectRatio)) throw new Error(`Unsupported aspect ratio "${aspectRatio}".`);
|
|
68
83
|
const modelProvider = String(args.model_provider ?? 'meshy');
|
|
@@ -91,7 +106,7 @@ export function resolveGenerationPlan(args, configuration = providerConfiguratio
|
|
|
91
106
|
if (kind === 'model_segment' && !args.source_job_id) {
|
|
92
107
|
throw new Error('model_segment requires source_job_id.');
|
|
93
108
|
}
|
|
94
|
-
const provider = IMAGE_KINDS.has(kind) ? imageModel.provider : modelProvider;
|
|
109
|
+
const provider = MATERIAL_KINDS.has(kind) ? 'fal' : IMAGE_KINDS.has(kind) ? imageModel.provider : modelProvider;
|
|
95
110
|
if (!configuration.providers[provider]) throw new Error(`${provider} is not configured.`);
|
|
96
111
|
if (textViaImage && !configuration.providers[imageModel.provider]) {
|
|
97
112
|
throw new Error(`${imageModel.provider} is not configured for the selected image model.`);
|
|
@@ -106,6 +121,70 @@ export function resolveGenerationPlan(args, configuration = providerConfiguratio
|
|
|
106
121
|
};
|
|
107
122
|
}
|
|
108
123
|
|
|
124
|
+
function extensionFor(contentType, fallbackName = '') {
|
|
125
|
+
if (contentType === 'image/jpeg') return 'jpg';
|
|
126
|
+
if (contentType === 'image/webp') return 'webp';
|
|
127
|
+
if (contentType === 'image/png') return 'png';
|
|
128
|
+
const extension = String(fallbackName).split('.').pop()?.toLowerCase();
|
|
129
|
+
return ['jpg', 'jpeg', 'png', 'webp'].includes(extension) ? extension : 'png';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Download every ephemeral Fal output and persist one grouped material bundle locally. */
|
|
133
|
+
export async function storeManagedMaterial(workspacePath, jobId, providerResult) {
|
|
134
|
+
const material = providerResult?.material;
|
|
135
|
+
if (!material || !Array.isArray(material.images)) throw new Error('Fal Patina returned no material bundle.');
|
|
136
|
+
const stored = [];
|
|
137
|
+
for (let index = 0; index < material.images.length; index += 1) {
|
|
138
|
+
const remote = material.images[index];
|
|
139
|
+
const downloaded = await downloadProviderAsset(remote.url, { maxBytes: 256 * 1024 * 1024 });
|
|
140
|
+
if (!downloaded.contentType.startsWith('image/')) throw new Error('Fal Patina returned a non-image material file.');
|
|
141
|
+
const role = remote.mapType ?? 'preview';
|
|
142
|
+
const file = publicFile(await saveObject(workspacePath, downloaded.bytes, {
|
|
143
|
+
contentType: downloaded.contentType,
|
|
144
|
+
kind: remote.mapType ? 'generated-material-map' : 'generated-material-preview',
|
|
145
|
+
name: `${role}-${index + 1}.${extensionFor(downloaded.contentType, downloaded.name)}`,
|
|
146
|
+
}));
|
|
147
|
+
stored.push({ file, mapType: remote.mapType });
|
|
148
|
+
}
|
|
149
|
+
const previews = stored.filter((entry) => entry.mapType === null).map((entry) => entry.file);
|
|
150
|
+
const variantsByMap = Object.fromEntries(PATINA_MAP_TYPES.map((map) => [
|
|
151
|
+
map,
|
|
152
|
+
stored.filter((entry) => entry.mapType === map).map((entry) => entry.file),
|
|
153
|
+
]));
|
|
154
|
+
const variantCount = Math.max(previews.length, ...Object.values(variantsByMap).map((files) => files.length), 1);
|
|
155
|
+
const variants = Array.from({ length: variantCount }, (_, outputIndex) => ({
|
|
156
|
+
outputIndex,
|
|
157
|
+
preview: previews[outputIndex] ?? null,
|
|
158
|
+
maps: Object.fromEntries(PATINA_MAP_TYPES.flatMap((map) => (
|
|
159
|
+
variantsByMap[map][outputIndex] ? [[map, variantsByMap[map][outputIndex]]] : []
|
|
160
|
+
))),
|
|
161
|
+
}));
|
|
162
|
+
const manifest = {
|
|
163
|
+
schema: 'toonlab-pbr-material-v1',
|
|
164
|
+
generationJobId: jobId,
|
|
165
|
+
model: 'fal-ai/patina/material',
|
|
166
|
+
provider: 'fal',
|
|
167
|
+
request: material.input,
|
|
168
|
+
generatedPrompt: material.prompt,
|
|
169
|
+
seed: material.seed,
|
|
170
|
+
timings: material.timings,
|
|
171
|
+
variants,
|
|
172
|
+
};
|
|
173
|
+
const manifestFile = publicFile(await saveObject(workspacePath, new TextEncoder().encode(JSON.stringify(manifest, null, 2)), {
|
|
174
|
+
contentType: 'application/json',
|
|
175
|
+
kind: 'generated-material-manifest',
|
|
176
|
+
name: `material-${jobId}.json`,
|
|
177
|
+
}));
|
|
178
|
+
const primary = variants[0]?.maps?.basecolor ?? variants[0]?.preview ?? Object.values(variants[0]?.maps ?? {})[0];
|
|
179
|
+
if (!primary) throw new Error('Fal Patina material bundle has no usable output.');
|
|
180
|
+
return {
|
|
181
|
+
file: primary,
|
|
182
|
+
previewFile: variants[0]?.preview ?? primary,
|
|
183
|
+
materialBundle: { ...manifest, manifestFile },
|
|
184
|
+
provider: 'fal',
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
109
188
|
async function referenceImage(workspacePath, relativePath) {
|
|
110
189
|
const file = await readObject(workspacePath, relativePath);
|
|
111
190
|
if (!String(file.mimeType).startsWith('image/')) throw new Error('Generation references must be images.');
|
|
@@ -143,6 +222,23 @@ export async function startManagedGeneration(workspacePath, args) {
|
|
|
143
222
|
style: args.style ?? 'stylized',
|
|
144
223
|
textViaImage: plan.textViaImage,
|
|
145
224
|
views: Array.isArray(args.view_paths) ? args.view_paths.slice(0, 4) : [],
|
|
225
|
+
...(plan.kind === 'material' ? {
|
|
226
|
+
enablePromptExpansion: args.enable_prompt_expansion,
|
|
227
|
+
enableSafetyChecker: args.enable_safety_checker,
|
|
228
|
+
imageSize: args.image_size,
|
|
229
|
+
imageUrl: args.image_url,
|
|
230
|
+
maps: args.maps,
|
|
231
|
+
maskUrl: args.mask_url,
|
|
232
|
+
numImages: args.num_images,
|
|
233
|
+
numInferenceSteps: args.num_inference_steps,
|
|
234
|
+
outputFormat: args.output_format,
|
|
235
|
+
seed: args.seed,
|
|
236
|
+
strength: args.strength,
|
|
237
|
+
tileSize: args.tile_size,
|
|
238
|
+
tileStride: args.tile_stride,
|
|
239
|
+
tilingMode: args.tiling_mode,
|
|
240
|
+
upscaleFactor: args.upscale_factor,
|
|
241
|
+
} : {}),
|
|
146
242
|
};
|
|
147
243
|
const job = await createGenerationJob(plan.provider, plan.kind, request);
|
|
148
244
|
try {
|
|
@@ -182,6 +278,7 @@ export async function startManagedGeneration(workspacePath, args) {
|
|
|
182
278
|
});
|
|
183
279
|
} else {
|
|
184
280
|
providerResult = await runProvider(plan.provider, {
|
|
281
|
+
...(plan.kind === 'material' ? request : {}),
|
|
185
282
|
aspectRatio: plan.aspectRatio,
|
|
186
283
|
kind: plan.kind,
|
|
187
284
|
model: plan.imageModel?.model,
|
|
@@ -200,6 +297,10 @@ export async function startManagedGeneration(workspacePath, args) {
|
|
|
200
297
|
...(conceptFile ? { conceptFile, pipeline: 'concept_image -> meshy-7' } : {}),
|
|
201
298
|
}));
|
|
202
299
|
}
|
|
300
|
+
if (providerResult.material) {
|
|
301
|
+
const result = await storeManagedMaterial(workspacePath, job.id, providerResult);
|
|
302
|
+
return publicGenerationJob(await finishGenerationJob(job.id, { result }));
|
|
303
|
+
}
|
|
203
304
|
if (providerResult.bytes instanceof Uint8Array) {
|
|
204
305
|
const file = await storeInlineImage(workspacePath, job.id, providerResult);
|
|
205
306
|
return publicGenerationJob(await finishGenerationJob(job.id, {
|
|
@@ -296,8 +397,8 @@ export async function saveManagedGeneration(id, { label } = {}) {
|
|
|
296
397
|
const job = await getGenerationJob(id);
|
|
297
398
|
if (!job) throw new Error('Generation job not found.');
|
|
298
399
|
if (job.status !== 'succeeded') throw new Error(`Generation job is ${job.status}, not succeeded.`);
|
|
299
|
-
if (!['text_to_model', 'image_to_model', 'multiview_to_model'].includes(job.kind) || !job.result?.file) {
|
|
300
|
-
throw new Error('Only succeeded 3D model jobs can be saved
|
|
400
|
+
if (!['text_to_model', 'image_to_model', 'multiview_to_model', 'material'].includes(job.kind) || !job.result?.file) {
|
|
401
|
+
throw new Error('Only succeeded 3D model or material jobs can be saved.');
|
|
301
402
|
}
|
|
302
403
|
const entry = {
|
|
303
404
|
aiGenerated: true,
|
|
@@ -308,7 +409,7 @@ export async function saveManagedGeneration(id, { label } = {}) {
|
|
|
308
409
|
provider: job.provider,
|
|
309
410
|
result: job.result,
|
|
310
411
|
source: 'generate',
|
|
311
|
-
type: 'generated-model',
|
|
412
|
+
type: job.kind === 'material' ? 'generated-material' : 'generated-model',
|
|
312
413
|
};
|
|
313
414
|
return saveLibraryEntry(entry);
|
|
314
415
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
create table if not exists creation_revisions (
|
|
2
|
+
id uuid primary key default gen_random_uuid(),
|
|
3
|
+
creation_id uuid not null references creations(id) on delete cascade,
|
|
4
|
+
revision_number integer not null check (revision_number > 0),
|
|
5
|
+
label text not null,
|
|
6
|
+
description text,
|
|
7
|
+
tags text[] not null default '{}',
|
|
8
|
+
document jsonb not null,
|
|
9
|
+
ai_generated boolean not null default false,
|
|
10
|
+
content_hash text not null,
|
|
11
|
+
save_source text not null default 'manual',
|
|
12
|
+
dependency_snapshot jsonb not null default '{}',
|
|
13
|
+
restored_from_revision_id uuid references creation_revisions(id) on delete set null,
|
|
14
|
+
version_name text,
|
|
15
|
+
version_tags text[] not null default '{}',
|
|
16
|
+
version_note text,
|
|
17
|
+
pinned boolean not null default false,
|
|
18
|
+
annotation_updated_at timestamptz,
|
|
19
|
+
created_at timestamptz not null default now(),
|
|
20
|
+
unique (creation_id, revision_number),
|
|
21
|
+
check (version_name is null or char_length(version_name) between 1 and 120),
|
|
22
|
+
check (version_note is null or char_length(version_note) <= 2000),
|
|
23
|
+
check (cardinality(version_tags) <= 10)
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
create index if not exists creation_revisions_creation_created
|
|
27
|
+
on creation_revisions (creation_id, revision_number desc);
|
|
28
|
+
create index if not exists creation_revisions_pinned
|
|
29
|
+
on creation_revisions (creation_id, pinned, revision_number desc);
|
|
30
|
+
create unique index if not exists creation_revisions_unique_name
|
|
31
|
+
on creation_revisions (creation_id, lower(version_name))
|
|
32
|
+
where version_name is not null;
|
|
33
|
+
|
|
34
|
+
alter table creations
|
|
35
|
+
add column if not exists current_revision_id uuid references creation_revisions(id) on delete set null;
|
|
36
|
+
|
|
37
|
+
with inserted as (
|
|
38
|
+
insert into creation_revisions (
|
|
39
|
+
creation_id, revision_number, label, description, tags, document,
|
|
40
|
+
ai_generated, content_hash, save_source
|
|
41
|
+
)
|
|
42
|
+
select
|
|
43
|
+
c.id,
|
|
44
|
+
1,
|
|
45
|
+
c.label,
|
|
46
|
+
c.description,
|
|
47
|
+
c.tags,
|
|
48
|
+
c.document,
|
|
49
|
+
c.ai_generated,
|
|
50
|
+
encode(digest(convert_to(jsonb_build_object(
|
|
51
|
+
'label', c.label,
|
|
52
|
+
'description', c.description,
|
|
53
|
+
'tags', c.tags,
|
|
54
|
+
'document', c.document,
|
|
55
|
+
'aiGenerated', c.ai_generated,
|
|
56
|
+
'dependencies', '{}'::jsonb
|
|
57
|
+
)::text, 'UTF8'), 'sha256'), 'hex'),
|
|
58
|
+
'migration'
|
|
59
|
+
from creations c
|
|
60
|
+
where c.current_revision_id is null
|
|
61
|
+
and not exists (
|
|
62
|
+
select 1 from creation_revisions r where r.creation_id = c.id
|
|
63
|
+
)
|
|
64
|
+
returning id, creation_id
|
|
65
|
+
)
|
|
66
|
+
update creations c
|
|
67
|
+
set current_revision_id = inserted.id
|
|
68
|
+
from inserted
|
|
69
|
+
where c.id = inserted.creation_id;
|
|
70
|
+
|
|
71
|
+
update creations c
|
|
72
|
+
set current_revision_id = (
|
|
73
|
+
select r.id
|
|
74
|
+
from creation_revisions r
|
|
75
|
+
where r.creation_id = c.id
|
|
76
|
+
order by r.revision_number desc
|
|
77
|
+
limit 1
|
|
78
|
+
)
|
|
79
|
+
where c.current_revision_id is null
|
|
80
|
+
and exists (select 1 from creation_revisions r where r.creation_id = c.id);
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
-- Repair creations imported after 0007 ran but before the import path began
|
|
2
|
+
-- committing its initial snapshot.
|
|
3
|
+
with inserted as (
|
|
4
|
+
insert into creation_revisions (
|
|
5
|
+
creation_id, revision_number, label, description, tags, document,
|
|
6
|
+
ai_generated, content_hash, save_source
|
|
7
|
+
)
|
|
8
|
+
select
|
|
9
|
+
c.id,
|
|
10
|
+
1,
|
|
11
|
+
c.label,
|
|
12
|
+
c.description,
|
|
13
|
+
c.tags,
|
|
14
|
+
c.document,
|
|
15
|
+
c.ai_generated,
|
|
16
|
+
encode(digest(convert_to(jsonb_build_object(
|
|
17
|
+
'label', c.label,
|
|
18
|
+
'description', c.description,
|
|
19
|
+
'tags', c.tags,
|
|
20
|
+
'document', c.document,
|
|
21
|
+
'aiGenerated', c.ai_generated,
|
|
22
|
+
'dependencies', '{}'::jsonb
|
|
23
|
+
)::text, 'UTF8'), 'sha256'), 'hex'),
|
|
24
|
+
'hardening-backfill'
|
|
25
|
+
from creations c
|
|
26
|
+
where c.current_revision_id is null
|
|
27
|
+
and not exists (select 1 from creation_revisions r where r.creation_id = c.id)
|
|
28
|
+
returning id, creation_id
|
|
29
|
+
)
|
|
30
|
+
update creations c
|
|
31
|
+
set current_revision_id = inserted.id
|
|
32
|
+
from inserted
|
|
33
|
+
where c.id = inserted.creation_id;
|
|
34
|
+
|
|
35
|
+
create or replace function toonlab_guard_creation_revision_snapshot()
|
|
36
|
+
returns trigger
|
|
37
|
+
language plpgsql
|
|
38
|
+
as $$
|
|
39
|
+
begin
|
|
40
|
+
if new.creation_id is distinct from old.creation_id
|
|
41
|
+
or new.revision_number is distinct from old.revision_number
|
|
42
|
+
or new.label is distinct from old.label
|
|
43
|
+
or new.description is distinct from old.description
|
|
44
|
+
or new.tags is distinct from old.tags
|
|
45
|
+
or new.document is distinct from old.document
|
|
46
|
+
or new.ai_generated is distinct from old.ai_generated
|
|
47
|
+
or new.content_hash is distinct from old.content_hash
|
|
48
|
+
or new.save_source is distinct from old.save_source
|
|
49
|
+
or new.dependency_snapshot is distinct from old.dependency_snapshot
|
|
50
|
+
or new.restored_from_revision_id is distinct from old.restored_from_revision_id
|
|
51
|
+
or new.created_at is distinct from old.created_at then
|
|
52
|
+
raise exception 'creation revision snapshots are immutable';
|
|
53
|
+
end if;
|
|
54
|
+
return new;
|
|
55
|
+
end;
|
|
56
|
+
$$;
|
|
57
|
+
|
|
58
|
+
create trigger creation_revisions_immutable_snapshot
|
|
59
|
+
before update on creation_revisions
|
|
60
|
+
for each row execute function toonlab_guard_creation_revision_snapshot();
|
|
61
|
+
|
|
62
|
+
create or replace function toonlab_guard_creation_revision_links()
|
|
63
|
+
returns trigger
|
|
64
|
+
language plpgsql
|
|
65
|
+
as $$
|
|
66
|
+
begin
|
|
67
|
+
if new.restored_from_revision_id is not null and not exists (
|
|
68
|
+
select 1 from creation_revisions r
|
|
69
|
+
where r.id = new.restored_from_revision_id and r.creation_id = new.creation_id
|
|
70
|
+
) then
|
|
71
|
+
raise exception 'restored revision must belong to the same creation';
|
|
72
|
+
end if;
|
|
73
|
+
return new;
|
|
74
|
+
end;
|
|
75
|
+
$$;
|
|
76
|
+
|
|
77
|
+
create trigger creation_revisions_same_creation_restore
|
|
78
|
+
before insert or update of restored_from_revision_id, creation_id on creation_revisions
|
|
79
|
+
for each row execute function toonlab_guard_creation_revision_links();
|
|
80
|
+
|
|
81
|
+
create or replace function toonlab_guard_current_creation_revision()
|
|
82
|
+
returns trigger
|
|
83
|
+
language plpgsql
|
|
84
|
+
as $$
|
|
85
|
+
begin
|
|
86
|
+
if new.current_revision_id is not null and not exists (
|
|
87
|
+
select 1 from creation_revisions r
|
|
88
|
+
where r.id = new.current_revision_id and r.creation_id = new.id
|
|
89
|
+
) then
|
|
90
|
+
raise exception 'current revision must belong to the same creation';
|
|
91
|
+
end if;
|
|
92
|
+
return new;
|
|
93
|
+
end;
|
|
94
|
+
$$;
|
|
95
|
+
|
|
96
|
+
create trigger creations_same_current_revision
|
|
97
|
+
before insert or update of current_revision_id, id on creations
|
|
98
|
+
for each row execute function toonlab_guard_current_creation_revision();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
-- A revision snapshot must never be rewritten merely because its source
|
|
2
|
+
-- revision is deleted. NO ACTION also permits a whole creation (and all of
|
|
3
|
+
-- its mutually-related revisions) to be removed by the parent cascade.
|
|
4
|
+
alter table creation_revisions
|
|
5
|
+
drop constraint creation_revisions_restored_from_revision_id_fkey;
|
|
6
|
+
|
|
7
|
+
alter table creation_revisions
|
|
8
|
+
add constraint creation_revisions_restored_from_revision_id_fkey
|
|
9
|
+
foreign key (restored_from_revision_id)
|
|
10
|
+
references creation_revisions(id)
|
|
11
|
+
on delete no action;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
-- Catalog-only entries intentionally retain an upstream or independently
|
|
2
|
+
-- mirrored public URL. They have no release-owned byte payload, so storing a
|
|
3
|
+
-- fabricated SHA-256, byte count, or content type would be misleading.
|
|
4
|
+
alter table catalog_assets
|
|
5
|
+
alter column sha256 drop not null,
|
|
6
|
+
alter column byte_size drop not null,
|
|
7
|
+
alter column content_type drop not null;
|
|
8
|
+
|
|
9
|
+
alter table catalog_assets
|
|
10
|
+
drop constraint if exists catalog_assets_integrity_matches_scope;
|
|
11
|
+
|
|
12
|
+
alter table catalog_assets
|
|
13
|
+
add constraint catalog_assets_integrity_matches_scope
|
|
14
|
+
check (
|
|
15
|
+
(
|
|
16
|
+
redistribution_scope = 'external-only'
|
|
17
|
+
and sha256 is null
|
|
18
|
+
and byte_size is null
|
|
19
|
+
and content_type is null
|
|
20
|
+
)
|
|
21
|
+
or
|
|
22
|
+
(
|
|
23
|
+
redistribution_scope <> 'external-only'
|
|
24
|
+
and sha256 is not null
|
|
25
|
+
and byte_size is not null
|
|
26
|
+
and content_type is not null
|
|
27
|
+
)
|
|
28
|
+
);
|