@iodes/releasekit 0.1.0 → 0.1.1

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.
Files changed (78) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +292 -110
  3. package/dist/cli.js +7 -5
  4. package/dist/content.d.ts +8 -0
  5. package/dist/content.js +5 -3
  6. package/dist/export.js +4 -3
  7. package/dist/images.d.ts +14 -5
  8. package/dist/images.js +32 -8
  9. package/dist/model.d.ts +29 -0
  10. package/dist/model.js +27 -3
  11. package/dist/prompts.d.ts +2 -2
  12. package/dist/prompts.js +24 -16
  13. package/dist/validate.js +2 -2
  14. package/examples/README.md +34 -14
  15. package/examples/backup-encryption/README.md +19 -0
  16. package/examples/backup-encryption/dark.png +0 -0
  17. package/examples/backup-encryption/dark.prompt.md +52 -0
  18. package/examples/backup-encryption/dimensions-edit.prompt.md +7 -0
  19. package/examples/backup-encryption/flat-render-requests.md +15 -0
  20. package/examples/backup-encryption/light.png +0 -0
  21. package/examples/backup-encryption/light.prompt.md +52 -0
  22. package/examples/backup-encryption/pair-review.md +18 -0
  23. package/examples/backup-encryption/scene.yaml +31 -0
  24. package/examples/connected-route/README.md +23 -0
  25. package/examples/connected-route/dark.png +0 -0
  26. package/examples/connected-route/dark.prompt.md +58 -0
  27. package/examples/connected-route/light.png +0 -0
  28. package/examples/connected-route/light.prompt.md +58 -0
  29. package/examples/connected-route/pair-review.md +22 -0
  30. package/examples/connected-route/render-requests.md +171 -0
  31. package/examples/connected-route/scene.yaml +59 -0
  32. package/examples/feature-briefs.yaml +96 -89
  33. package/examples/location-preferences/README.md +18 -0
  34. package/examples/location-preferences/dark.png +0 -0
  35. package/examples/location-preferences/dark.prompt.md +52 -0
  36. package/examples/location-preferences/light.png +0 -0
  37. package/examples/location-preferences/light.prompt.md +52 -0
  38. package/examples/location-preferences/pair-review.md +17 -0
  39. package/examples/location-preferences/scene.yaml +26 -0
  40. package/examples/provided-media/README.md +15 -0
  41. package/examples/provided-media/scene.yaml +22 -0
  42. package/examples/queue-action/README.md +15 -15
  43. package/examples/queue-action/alignment-edit.prompt.md +8 -8
  44. package/examples/queue-action/dark.prompt.md +58 -58
  45. package/examples/queue-action/light.prompt.md +58 -58
  46. package/examples/queue-action/pair-review.md +33 -33
  47. package/examples/queue-action/scene.yaml +41 -41
  48. package/examples/release-notes.en-US.json +56 -56
  49. package/examples/release-notes.ko-KR.json +56 -56
  50. package/examples/storage-breakdown/README.md +18 -0
  51. package/examples/storage-breakdown/dark.png +0 -0
  52. package/examples/storage-breakdown/dark.prompt.md +52 -0
  53. package/examples/storage-breakdown/light.png +0 -0
  54. package/examples/storage-breakdown/light.prompt.md +52 -0
  55. package/examples/storage-breakdown/pair-review.md +18 -0
  56. package/examples/storage-breakdown/scene.yaml +28 -0
  57. package/examples/tablet-reading/README.md +18 -0
  58. package/examples/tablet-reading/content-edit.prompt.md +7 -0
  59. package/examples/tablet-reading/dark.png +0 -0
  60. package/examples/tablet-reading/dark.prompt.md +52 -0
  61. package/examples/tablet-reading/light.png +0 -0
  62. package/examples/tablet-reading/light.prompt.md +52 -0
  63. package/examples/tablet-reading/pair-review.md +18 -0
  64. package/examples/tablet-reading/scene.yaml +29 -0
  65. package/kit/references/composition-recipes.md +89 -73
  66. package/kit/references/format.md +26 -24
  67. package/kit/references/media-sources.md +34 -0
  68. package/kit/references/theme-pairing.md +55 -53
  69. package/kit/references/visual-language.md +73 -69
  70. package/kit/references/workflow.md +24 -24
  71. package/kit/references/writing.md +27 -27
  72. package/kit/skills/releasekit-draft/SKILL.md +10 -10
  73. package/kit/skills/releasekit-image/SKILL.md +16 -16
  74. package/kit/skills/releasekit-review/SKILL.md +12 -12
  75. package/kit/skills/releasekit-translate/SKILL.md +10 -10
  76. package/package.json +54 -52
  77. package/schemas/bundle.schema.json +26 -1
  78. package/schemas/visual.schema.json +42 -0
package/dist/images.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import sharp from 'sharp';
4
- import { themes, theme } from './model.js';
4
+ import { themes, assetVariant, imageSource, activeVariants } from './model.js';
5
5
  import { Project, editable } from './project.js';
6
6
  import { readVisual, checkReferenceFiles } from './content.js';
7
7
  import { digest, identifier, exists, write, writeYaml } from './files.js';
@@ -27,10 +27,11 @@ export async function planImages(project, version) {
27
27
  let ready = 0;
28
28
  for (const note of release.notes.filter(n => n.image)) {
29
29
  const visual = await readVisual(project, version, note.id);
30
+ const source = imageSource(visual.scene);
30
31
  const pair = visual.variants;
31
32
  const invalidPair = release.visuals.themes === 'both' && pair.dark && pair.light &&
32
33
  (pair.dark.sha256 === pair.light.sha256 || pair.dark.width !== pair.light.width || pair.dark.height !== pair.light.height);
33
- for (const variant of themes(release.visuals)) {
34
+ for (const variant of activeVariants(visual, release.visuals)) {
34
35
  const expected = sceneHash(visual.scene, release.visuals, variant);
35
36
  const asset = visual.variants[variant];
36
37
  const file = asset && await project.releaseFile(version, asset.file);
@@ -41,6 +42,16 @@ export async function planImages(project, version) {
41
42
  continue;
42
43
  }
43
44
  editable(release);
45
+ if (source === 'provided') {
46
+ requests.push({
47
+ note: note.id, theme: variant, reason: present ? 'stale' : 'missing', action: 'provide',
48
+ promptFile: null, compositionReference: null,
49
+ instruction: `Use an existing approved image or ask for a capture, photograph, or content asset for "${visual.scene.subject}". Import the selected file with --theme ${variant}. Do not synthesize a replacement.`,
50
+ });
51
+ continue;
52
+ }
53
+ if (variant === 'shared')
54
+ throw new Error('Generated images require a dark or light variant.');
44
55
  await checkReferenceFiles(project, visual.scene.references);
45
56
  const promptFile = await project.releaseFile(version, `prompts/${note.id}.${variant}.md`);
46
57
  await write(promptFile, imagePrompt(visual.scene, release.visuals, variant));
@@ -52,12 +63,14 @@ export async function planImages(project, version) {
52
63
  if (await exists(reference) && digest(await fs.readFile(reference)) === other.sha256)
53
64
  compositionReference = reference;
54
65
  }
55
- requests.push({ note: note.id, theme: variant, reason: present ? 'stale' : 'missing', promptFile, compositionReference });
66
+ requests.push({ note: note.id, theme: variant, reason: present ? 'stale' : 'missing', action: 'generate', promptFile, compositionReference });
56
67
  }
57
68
  }
58
69
  return {
59
70
  version, configuredThemes: themes(release.visuals), requestedAssets: ready + requests.length,
60
71
  readyAssets: ready, pendingAssets: requests.length, requests,
72
+ generationRequests: requests.filter(request => request.action === 'generate').length,
73
+ providedRequests: requests.filter(request => request.action === 'provide').length,
61
74
  costNote: 'Counts describe required output assets, not provider prices or a guarantee of one tool call per asset. No image service was called.',
62
75
  };
63
76
  }
@@ -65,12 +78,23 @@ export async function importImage(project, version, noteId, variant, source) {
65
78
  const release = await project.release(version);
66
79
  editable(release);
67
80
  identifier(noteId);
68
- theme.parse(variant);
81
+ assetVariant.parse(variant);
69
82
  if (!release.notes.some(n => n.id === noteId && n.image))
70
83
  throw new Error(`No image-enabled note named ${noteId}.`);
71
- if (!themes(release.visuals).includes(variant))
72
- throw new Error(`Theme ${variant} is not enabled for this release. Update the project setting and sync the draft first.`);
73
84
  const visual = await readVisual(project, version, noteId);
85
+ const provided = imageSource(visual.scene) === 'provided';
86
+ if (variant === 'shared') {
87
+ if (!provided)
88
+ throw new Error('Only supplied images can use a shared asset.');
89
+ if (visual.variants.dark || visual.variants.light)
90
+ throw new Error('Remove the themed variant entries before switching to one shared supplied image.');
91
+ }
92
+ else {
93
+ if (!themes(release.visuals).includes(variant))
94
+ throw new Error(`Theme ${variant} is not enabled for this release. Update the project setting and sync the draft first.`);
95
+ if (visual.variants.shared)
96
+ throw new Error('Remove the shared variant entry before switching to distinct supplied theme variants.');
97
+ }
74
98
  const bytes = await fs.readFile(path.resolve(project.root, source));
75
99
  const inspected = await inspectImage(bytes);
76
100
  const file = `assets/${noteId}.${variant}.${inspected.sha256.slice(0, 12)}.${inspected.extension}`;
@@ -90,7 +114,7 @@ export async function importImage(project, version, noteId, variant, source) {
90
114
  }
91
115
  export async function validateImages(project, version, noteId, visual, errors, warnings) {
92
116
  const release = await project.release(version);
93
- for (const variant of themes(release.visuals)) {
117
+ for (const variant of activeVariants(visual, release.visuals)) {
94
118
  const expected = sceneHash(visual.scene, release.visuals, variant);
95
119
  const asset = visual.variants[variant];
96
120
  if (!asset) {
@@ -105,7 +129,7 @@ export async function validateImages(project, version, noteId, visual, errors, w
105
129
  errors.push(`${noteId}: ${variant} asset changed after import; import the selected file again.`);
106
130
  }
107
131
  const requestedRatio = release.visuals.width / release.visuals.height;
108
- if (Math.abs(actual.width / actual.height / requestedRatio - 1) > 0.05) {
132
+ if (imageSource(visual.scene) === 'generated' && Math.abs(actual.width / actual.height / requestedRatio - 1) > 0.05) {
109
133
  warnings.push(`${noteId}: ${variant} aspect ratio differs from the project target; review its framing.`);
110
134
  }
111
135
  }
package/dist/model.d.ts CHANGED
@@ -7,6 +7,12 @@ export declare const theme: z.ZodEnum<{
7
7
  light: "light";
8
8
  }>;
9
9
  export type Theme = z.infer<typeof theme>;
10
+ export declare const assetVariant: z.ZodEnum<{
11
+ dark: "dark";
12
+ light: "light";
13
+ shared: "shared";
14
+ }>;
15
+ export type AssetVariant = z.infer<typeof assetVariant>;
10
16
  export declare const paletteSchema: z.ZodObject<{
11
17
  canvas: z.ZodString;
12
18
  surface: z.ZodString;
@@ -193,6 +199,10 @@ export declare const sceneSchema: z.ZodObject<{
193
199
  "symbol-pair": "symbol-pair";
194
200
  "ui-detail": "ui-detail";
195
201
  }>;
202
+ source: z.ZodOptional<z.ZodEnum<{
203
+ generated: "generated";
204
+ provided: "provided";
205
+ }>>;
196
206
  subject: z.ZodString;
197
207
  message: z.ZodString;
198
208
  focus: z.ZodString;
@@ -225,6 +235,10 @@ export declare const visualSchema: z.ZodObject<{
225
235
  "symbol-pair": "symbol-pair";
226
236
  "ui-detail": "ui-detail";
227
237
  }>;
238
+ source: z.ZodOptional<z.ZodEnum<{
239
+ generated: "generated";
240
+ provided: "provided";
241
+ }>>;
228
242
  subject: z.ZodString;
229
243
  message: z.ZodString;
230
244
  focus: z.ZodString;
@@ -251,6 +265,13 @@ export declare const visualSchema: z.ZodObject<{
251
265
  width: z.ZodNumber;
252
266
  height: z.ZodNumber;
253
267
  }, z.core.$strict>>;
268
+ shared: z.ZodOptional<z.ZodObject<{
269
+ file: z.ZodString;
270
+ sha256: z.ZodString;
271
+ sceneHash: z.ZodString;
272
+ width: z.ZodNumber;
273
+ height: z.ZodNumber;
274
+ }, z.core.$strict>>;
254
275
  }, z.core.$strict>;
255
276
  }, z.core.$strict>;
256
277
  export type Visual = z.infer<typeof visualSchema>;
@@ -296,6 +317,7 @@ export declare const bundleSchema: z.ZodObject<{
296
317
  fallbackTheme: z.ZodEnum<{
297
318
  dark: "dark";
298
319
  light: "light";
320
+ shared: "shared";
299
321
  }>;
300
322
  variants: z.ZodObject<{
301
323
  dark: z.ZodOptional<z.ZodObject<{
@@ -308,6 +330,11 @@ export declare const bundleSchema: z.ZodObject<{
308
330
  width: z.ZodNumber;
309
331
  height: z.ZodNumber;
310
332
  }, z.core.$strict>>;
333
+ shared: z.ZodOptional<z.ZodObject<{
334
+ src: z.ZodString;
335
+ width: z.ZodNumber;
336
+ height: z.ZodNumber;
337
+ }, z.core.$strict>>;
311
338
  }, z.core.$strict>;
312
339
  }, z.core.$strict>>;
313
340
  }, z.core.$strict>>;
@@ -315,4 +342,6 @@ export declare const bundleSchema: z.ZodObject<{
315
342
  }, z.core.$strict>;
316
343
  export type Bundle = z.infer<typeof bundleSchema>;
317
344
  export declare function themes(policy: VisualPolicy): Theme[];
345
+ export declare function imageSource(scene: Scene): 'generated' | 'provided';
346
+ export declare function activeVariants(visual: Visual, policy: VisualPolicy): AssetVariant[];
318
347
  export declare function defaultConfig(product: string): ProjectConfig;
package/dist/model.js CHANGED
@@ -3,6 +3,7 @@ export const segment = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._+-]{0,95}$/);
3
3
  export const sha = z.string().regex(/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/);
4
4
  export const locale = z.string().regex(/^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/);
5
5
  export const theme = z.enum(['dark', 'light']);
6
+ export const assetVariant = z.enum(['dark', 'light', 'shared']);
6
7
  const color = z.string().regex(/^#[a-fA-F0-9]{6}$/);
7
8
  export const paletteSchema = z.strictObject({
8
9
  canvas: color, surface: color, raised: color, primary: color, secondary: color, divider: color,
@@ -47,6 +48,7 @@ export const archetypeSchema = z.enum([
47
48
  ]);
48
49
  export const sceneSchema = z.strictObject({
49
50
  archetype: archetypeSchema,
51
+ source: z.enum(['generated', 'provided']).optional(),
50
52
  subject: z.string().min(1), message: z.string().min(1),
51
53
  focus: z.string().min(1), composition: z.string().min(1),
52
54
  context: z.string(), elements: z.array(z.string()),
@@ -60,7 +62,7 @@ export const assetSchema = z.strictObject({
60
62
  });
61
63
  export const visualSchema = z.strictObject({
62
64
  schemaVersion: z.literal(1), scene: sceneSchema,
63
- variants: z.strictObject({ dark: assetSchema.optional(), light: assetSchema.optional() }),
65
+ variants: z.strictObject({ dark: assetSchema.optional(), light: assetSchema.optional(), shared: assetSchema.optional() }),
64
66
  });
65
67
  export const evidenceSchema = z.strictObject({
66
68
  schemaVersion: z.literal(1), source: sourceSchema,
@@ -77,8 +79,8 @@ export const bundleSchema = z.strictObject({
77
79
  notes: z.array(z.strictObject({
78
80
  id: segment, category: noteMetaSchema.shape.category, title: z.string(), bodyMarkdown: z.string(),
79
81
  image: z.strictObject({
80
- alt: z.string(), fallbackTheme: theme,
81
- variants: z.strictObject({ dark: exportedImage.optional(), light: exportedImage.optional() }),
82
+ alt: z.string(), fallbackTheme: assetVariant,
83
+ variants: z.strictObject({ dark: exportedImage.optional(), light: exportedImage.optional(), shared: exportedImage.optional() }),
82
84
  }).nullable(),
83
85
  })),
84
86
  })),
@@ -86,6 +88,28 @@ export const bundleSchema = z.strictObject({
86
88
  export function themes(policy) {
87
89
  return policy.themes === 'both' ? ['dark', 'light'] : [policy.themes];
88
90
  }
91
+ export function imageSource(scene) {
92
+ if (scene.archetype === 'object-detail' || scene.archetype === 'editorial-scene') {
93
+ if (scene.source === 'generated')
94
+ throw new Error(`${scene.archetype} requires a supplied capture, photograph, or content image. Set source to provided.`);
95
+ return 'provided';
96
+ }
97
+ return scene.source ?? 'generated';
98
+ }
99
+ export function activeVariants(visual, policy) {
100
+ const provided = imageSource(visual.scene) === 'provided';
101
+ const { dark, light, shared } = visual.variants;
102
+ if (shared) {
103
+ if (!provided)
104
+ throw new Error('Only supplied images can use a shared asset.');
105
+ if (dark || light)
106
+ throw new Error('Choose a shared supplied image or distinct theme variants; do not mix both.');
107
+ return ['shared'];
108
+ }
109
+ if (provided && !dark && !light)
110
+ return ['shared'];
111
+ return themes(policy);
112
+ }
89
113
  export function defaultConfig(product) {
90
114
  return {
91
115
  schemaVersion: 1, product, sourceLocale: 'ko-KR', locales: ['ko-KR', 'en-US'],
package/dist/prompts.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { type Scene, type Theme, type VisualPolicy } from './model.js';
1
+ import { type AssetVariant, type Scene, type Theme, type VisualPolicy } from './model.js';
2
2
  export declare const recipes: Record<Scene['archetype'], {
3
3
  framing: string;
4
4
  treatment: string;
5
5
  review: string;
6
6
  }>;
7
- export declare function sceneHash(scene: Scene, policy: VisualPolicy, variant: Theme): string;
7
+ export declare function sceneHash(scene: Scene, policy: VisualPolicy, variant: AssetVariant): string;
8
8
  export declare function imagePrompt(scene: Scene, policy: VisualPolicy, variant: Theme): string;
package/dist/prompts.js CHANGED
@@ -1,14 +1,14 @@
1
1
  import { canonical, digest } from './files.js';
2
- import {} from './model.js';
2
+ import { imageSource } from './model.js';
3
3
  export const recipes = {
4
4
  'icon-tile': {
5
- framing: 'Center one compact symbol or rounded tile. Keep its width around 1624% of the canvas; allow broad uninterrupted negative space. Use optical centering.',
6
- treatment: 'A precise filled glyph or restrained softly modeled icon. One symbol and at most one small status badge. A tile is optional when the naked silhouette is clearer.',
5
+ framing: 'Center one small flat rounded-square tile, normally 20–24% of the canvas width. Keep the glyph around 5065% of the tile width. Use optical centering and broad uninterrupted negative space. A naked glyph is appropriate only when the scene explicitly calls for it.',
6
+ treatment: 'Use a crisp flat 2D filled glyph in one neutral gray value, with negative space for internal details. Keep the canvas and tile uniform and untextured. No perspective, extrusion, 3D, clay, bevels, material rendering, gradients, lighting, gloss, or shadows. Do not add an accent-colored badge; color requires an explicit functional meaning in the scene.',
7
7
  review: 'The symbol must communicate the stated capability or status. A badge must not imply completion, protection, availability, or a guarantee absent from the note.',
8
8
  },
9
9
  'symbol-pair': {
10
10
  framing: 'Place two similarly weighted symbols on one horizontal optical axis, centered as a group; a short low-contrast divider can separate them.',
11
- treatment: 'Communicate one relationship. Match stroke weight, corner treatment, and perceived size. Use an arrow only when direction itself is part of the feature.',
11
+ treatment: 'Communicate one relationship with flat 2D filled glyphs. Match visual weight, corner treatment, and perceived size. Use an arrow only when direction itself is part of the feature. Prefer neutral gray; use color only for a stated interaction or semantic status. No rendered materials or sculpted 3D symbols.',
12
12
  review: 'Check which two concepts are related and whether the relationship is directional. A connector must not imply transfer, synchronization, or automation unless supported by the note.',
13
13
  },
14
14
  'ui-detail': {
@@ -18,18 +18,18 @@ export const recipes = {
18
18
  },
19
19
  'device-view': {
20
20
  framing: 'Use one front-facing device or display at roughly 28–48% of the canvas width. A bottom crop is allowed when it enlarges the relevant feature; preserve the entire focus area.',
21
- treatment: 'Keep the frame unobtrusive and the screen grounded in supplied product evidence. Use a single device unless cross-device interaction is the feature. Device materials keep their natural appearance in both themes.',
21
+ treatment: 'Keep any generated frame flat and schematic, with the screen grounded in supplied product evidence. Use a single device unless cross-device interaction is the feature. Choose a supplied capture when actual device appearance matters; do not invent a modeled hardware product.',
22
22
  review: 'Check the actual device count, screen content, and relationship between devices. Do not imply an unsupported device, connection, or application theme.',
23
23
  },
24
24
  'object-detail': {
25
- framing: 'Show a close view of the relevant product part; choose a restrained three-quarter or orthographic angle. Preserve feature-defining silhouettes and enough context to recognize the object.',
26
- treatment: 'Soft studio lighting and clean matte or authentic product materials. Use quiet depth, contact shadows, and sparse highlights; accent the changed part only. Do not invent an unrelated physical product.',
25
+ framing: 'Use a supplied image of the relevant product part and retain enough context to recognize it.',
26
+ treatment: 'Request an authentic product capture, photograph, or approved asset. Preserve the supplied appearance; this category does not create a new physical object or 3D illustration.',
27
27
  review: 'Check the feature-defining shape, scale, assembly, contact points, and material against product references. A highlight must not invent a component or change how the object works.',
28
28
  },
29
29
  'spatial-view': {
30
- framing: 'Let a route, spatial diagram, or scene fill the canvas when its topology is necessary. Choose top-down or a single consistent elevated viewpoint; keep the main path or selection readable.',
31
- treatment: 'Suppress background detail, use simplified geometry, and preserve conventional route, warning, and selection colors. Display only the layers needed to explain the changed behavior.',
32
- review: 'Check positions, connections, direction, scale relationships, and layer meanings against the scene. Routes must remain connected where required, and the graphic must not imply unsupported locations or navigation behavior.',
30
+ framing: 'Choose the geographic or diagrammatic scale needed to explain the feature, with one consistent viewpoint. A map can reach the canvas edges; reserve quiet space for a supported summary only when the brief calls for it. Do not impose a split layout, coast, road network, or endpoint markers on every spatial scene.',
31
+ treatment: 'For maps, retain enough fine, low-contrast context to read as a map. Reduce its contrast before deleting its structure: distinguish minor streets, major connections, and land or water through thin linework and flat values. Keep the active route or selection dominant without turning streets into oversized roads or padded checkerboard blocks. Use diagrammatic simplification when relationships alone are the subject. Preserve semantic colors; avoid decorative relief, bevels, textures, and lighting.',
32
+ review: 'Check positions, connections, direction, scale relationships, and layer meanings against the scene. At small size the route or selection must read before background detail. Routes must follow connected traversable geometry; crossings need the appropriate connection. Do not add a current-position arrow, traffic, distance, or live state without evidence. Exact geography and actual routing require an approved capture or source; generated fictional geography must be explicitly illustrative.',
33
33
  },
34
34
  'data-view': {
35
35
  framing: 'Focus on one panel or device showing one dominant visualization and a few supporting rows. Give the primary metric or interaction clear breathing room.',
@@ -37,17 +37,21 @@ export const recipes = {
37
37
  review: 'Check category identity, axes, units, relative values, totals, legends, and any selected filter when present. Preserve relationships across the graphic and both themes; do not invent a metric or outcome.',
38
38
  },
39
39
  'editorial-scene': {
40
- framing: 'Compose one coherent scene around the actual announced experience. Establish a clear foreground subject and quiet supporting background. Keep it legible as a small release card.',
41
- treatment: 'Use natural color, rich materials, or playful elements only when they belong to the feature. Reserve this treatment for a content or seasonal experience; it is not the default for routine fixes or settings.',
40
+ framing: 'Use a supplied capture or approved image of the announced content, with a crop that keeps the subject recognizable.',
41
+ treatment: 'Request actual content artwork or a screenshot. Preserve its appearance and colors; this category does not invent an editorial still life, illustration, or promotional scene.',
42
42
  review: 'Check that the depicted subject and experience match the announced content. Keep essential object relationships coherent and avoid added features, factual promises, or unrelated scenery.',
43
43
  },
44
44
  };
45
45
  export function sceneHash(scene, policy, variant) {
46
+ if (variant === 'shared' || scene.source === 'provided')
47
+ return digest(canonical({ scene, variant, source: 'provided' }));
46
48
  // Other themes and their palettes must not invalidate this already accepted render.
47
49
  const { themes: _themes, dark: _dark, light: _light, ...appearance } = policy;
48
50
  return digest(canonical({ scene, appearance, theme: variant, palette: policy[variant] }));
49
51
  }
50
52
  export function imagePrompt(scene, policy, variant) {
53
+ if (imageSource(scene) === 'provided')
54
+ throw new Error('This scene needs a supplied image. Request or import the source instead of generating an illustration.');
51
55
  const recipe = recipes[scene.archetype];
52
56
  const palette = policy[variant];
53
57
  const list = (items) => items.length ? items.map(item => `- ${item}`).join('\n') : '- None';
@@ -57,10 +61,14 @@ export function imagePrompt(scene, policy, variant) {
57
61
  `## Composition contract\nArchetype: ${scene.archetype}\nTarget canvas: ${policy.width} × ${policy.height} pixels; landscape ${policy.width}:${policy.height}. Produce a single image, not a dark/light collage.\n${recipe.framing}\nSpecific scene layout: ${scene.composition}\nElements:\n${list(scene.elements)}\n\n` +
58
62
  `## Visual treatment\n${recipe.treatment}\nFavor visual precision, quiet hierarchy, and one instantly understandable feature. Small-screen clarity takes priority over decorative detail. Treat the specified element inventory as complete. Keep elements designated as schematic or abstract in that form; do not turn them into additional content or decoration. Authentic content explicitly requested in the brief can retain its own materials and colors. Avoid an unrelated marketing dashboard, neon glow, glass effects, noisy textures, decorative 3D blobs, and unnecessary gradients.\n\n` +
59
63
  `## ${variant === 'dark' ? 'Dark' : 'Light'} theme roles\n` +
60
- `Canvas ${palette.canvas}; base surface ${palette.surface}; raised surface ${palette.raised}; main neutral symbol ${palette.primary}; secondary detail ${palette.secondary}; divider ${palette.divider}; interaction accent ${policy.accent}.\n` +
61
- (variant === 'dark'
62
- ? 'Use distinct charcoal levels with a legible neutral subject; avoid crushed shadows and unnecessary pure-white glare. Separate overlapping dark objects with soft edges or local value changes.\n'
63
- : 'Use a near-white canvas, subtle surface separation, restrained contact shadows, and medium-dark neutral symbols. Avoid both flat white-on-white disappearance and thick dark outlines.\n') +
64
+ `Canvas ${palette.canvas}; base surface ${palette.surface}; raised surface ${palette.raised}; main neutral symbol ${palette.primary}; secondary detail ${palette.secondary}; divider ${palette.divider}; interaction accent ${policy.accent}. Use the accent only when the scene assigns it a functional meaning.\n` +
65
+ (scene.archetype === 'icon-tile' || scene.archetype === 'symbol-pair'
66
+ ? `Use uniform flat color areas and crisp negative space. If a tile is present, use ${variant === 'dark' ? palette.surface : palette.raised} for its flat fill. Separate the neutral glyph and its background by value alone. Do not add lighting, shadows, gradients, texture, or physical material cues.\n`
67
+ : scene.archetype === 'spatial-view'
68
+ ? 'Use flat value separation and crisp linework. Keep background layers subordinate to the focal route or selection in this theme. Do not add contact shadows, studio lighting, bevels, or material shading. A local fade into quiet space is allowed only when specified by the scene.\n'
69
+ : variant === 'dark'
70
+ ? 'Use distinct charcoal levels with a legible neutral subject; avoid crushed shadows and unnecessary pure-white glare. Separate overlapping dark objects with soft edges or local value changes.\n'
71
+ : 'Use a near-white canvas, subtle surface separation, restrained contact shadows, and medium-dark neutral symbols. Avoid both flat white-on-white disappearance and thick dark outlines.\n') +
64
72
  `Treat these colors as presentation roles, not a global recoloring filter. Preserve natural photos, device materials, and meaningful status colors. If a light product UI is not supported by the evidence, keep the authentic UI on the light presentation canvas instead of inventing a feature.\n\n` +
65
73
  `## Pair invariants\nThe other theme must use the same object count, positions, scale, crop, camera, UI topology, selected state, chart values, allowed labels, and feature meaning. Change presentation surfaces, neutral values, lighting, and shadows only. Preserve semantic accent hues. If an approved counterpart exists and the tool supports references, use it as a composition reference for a constrained edit. Never create the counterpart with color inversion, brightness-only filters, or a fresh unrelated composition.\nSpecific invariants:\n${list(scene.preserve)}\n\n` +
66
74
  `## Text and references\n` +
package/dist/validate.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as fs from 'node:fs/promises';
2
- import {} from './model.js';
2
+ import { imageSource } from './model.js';
3
3
  import { Project, editable } from './project.js';
4
4
  import { canonical, digest, readNote, noteHash, identifier } from './files.js';
5
5
  import { readVisual, checkReferenceFiles } from './content.js';
@@ -60,7 +60,7 @@ export async function validate(project, version) {
60
60
  if (note.image) {
61
61
  try {
62
62
  const visual = await readVisual(project, version, note.id);
63
- if (release.status === 'draft')
63
+ if (release.status === 'draft' && imageSource(visual.scene) === 'generated')
64
64
  await checkReferenceFiles(project, visual.scene.references);
65
65
  await validateImages(project, version, note.id, visual, errors, warnings);
66
66
  }
@@ -1,14 +1,34 @@
1
- # Example release content
2
-
3
- These fictional notes illustrate the public display contract. They are not release announcements for this toolkit or claims about a shipped product.
4
-
5
- - [Korean bundle](release-notes.ko-KR.json)
6
- - [English bundle](release-notes.en-US.json)
7
- - [Original paired illustration, scene, prompts, and review](queue-action/README.md)
8
- - [Independent feature briefs](feature-briefs.yaml): an encryption symbol, a static setting, and a data breakdown
9
-
10
- The independent briefs pair each fictional note with a different scene and its correctness constraints. They are authoring examples, separate from the public bundle format. The prompt compiler uses only the selected brief and recipe for each note; they do not inherit the list interaction from the raster example. The briefs do not claim to be generated or reviewed raster assets.
11
-
12
- Each bundle contains version 1.4.0 followed by 1.3.0 and 1.2.0. The explicit `previous` links define that order. The `queue-action` note appears in two different version groups because each describes that version's change; consumers retain both entries.
13
-
14
- Both locales reference the same selected dark and light PNGs. Choose `image.variants[theme]`, falling back to `image.variants[image.fallbackTheme]` only when the requested variant is absent. Text-only notes use `image: null`. These are content examples; build the surrounding scrolling interface in the consumer application.
1
+ # Example release content
2
+
3
+ These fictional notes illustrate the public display contract. They are not release announcements for this toolkit or claims about a shipped product.
4
+
5
+ - [Korean bundle](release-notes.ko-KR.json)
6
+ - [English bundle](release-notes.en-US.json)
7
+ - [Generated explanations, scenes, prompts, and reviews](#composition-gallery)
8
+ - [Supplied-image workflow](provided-media/README.md) for real product details and content previews
9
+ - [Independent feature briefs](feature-briefs.yaml): an encryption symbol, a static setting, and a data breakdown
10
+
11
+ The independent briefs pair each fictional note with a different scene and its correctness constraints. They are authoring examples, separate from the public bundle format. The prompt compiler uses only the selected brief and recipe for each note; they do not inherit the list interaction from the raster example. The encryption and storage briefs also have rendered examples below; the quiet-hours brief remains a text-only alternative for `ui-detail`.
12
+
13
+ ## Composition gallery
14
+
15
+ The gallery demonstrates generated explanations. Each displayed folder contains a shared `scene.yaml`, compiled prompts, selected PNGs, and a review. These are fictional authoring references, not default layouts. Physical details and actual content previews use the supplied-image workflow rather than synthetic examples.
16
+
17
+ | Recipe and example | Dark | Light |
18
+ | --- | --- | --- |
19
+ | [`icon-tile`: backup encryption](backup-encryption/README.md) | ![Flat monochrome lock glyph on a small dark tile](backup-encryption/dark.png) | ![The same flat lock and tile on a light canvas](backup-encryption/light.png) |
20
+ | [`symbol-pair`: location preferences](location-preferences/README.md) | ![Adjustment glyph associated with a blue location pin on charcoal](location-preferences/dark.png) | ![The same adjustment glyph and location pin on near-white](location-preferences/light.png) |
21
+ | [`ui-detail`: queue action](queue-action/README.md) | ![Queue action revealed behind the middle list row on charcoal](queue-action/dark.png) | ![The same queue action and list geometry on near-white](queue-action/light.png) |
22
+ | [`device-view`: tablet reading](tablet-reading/README.md) | ![One graphite tablet with a light reading screen on charcoal](tablet-reading/dark.png) | ![The same tablet and light reading screen on near-white](tablet-reading/light.png) |
23
+ | [`spatial-view`: connected route](connected-route/README.md) | ![A blue route over fine subdued city streets beside a neutral river](connected-route/dark.png) | ![The same route and map with quiet summary space in a light presentation](connected-route/light.png) |
24
+ | [`data-view`: storage breakdown](storage-breakdown/README.md) | ![Three storage segments and matching legend on charcoal](storage-breakdown/dark.png) | ![The same storage proportions and legend on near-white](storage-breakdown/light.png) |
25
+
26
+ Use the tablet example to see how a light-only product screen stays light on both presentation canvases. Use an actual capture when device or interface fidelity matters. The storage values are illustrative, and the route has no real geographic identity.
27
+
28
+ `object-detail` and `editorial-scene` require an approved photograph, screenshot, or content asset. If none is available, the plan returns a supplied-image request with no generation prompt. [The supplied-media example](provided-media/README.md) shows that pending state and shared-asset import. Earlier synthetic physical-object and decorative-content explorations are retired and excluded from the package.
29
+
30
+ ## Public bundles
31
+
32
+ Each bundle contains version 1.4.0 followed by 1.3.0 and 1.2.0. The explicit `previous` links define that order. The `queue-action` note appears in two different version groups because each describes that version's change; consumers retain both entries.
33
+
34
+ Both locales reference the same selected dark and light PNGs. Choose `image.variants[theme]`, falling back to `image.variants[image.fallbackTheme]` only when the requested variant is absent. Text-only notes use `image: null`. These are content examples; build the surrounding scrolling interface in the consumer application.
@@ -0,0 +1,19 @@
1
+ # Backup encryption
2
+
3
+ An original fictional `icon-tile` example: Local backup files can be saved with encryption enabled.
4
+
5
+ Fictional capability illustration, not a shipped product claim. The lock represents encryption of local backup files only; it does not claim protection of every product surface.
6
+
7
+ | Dark | Light |
8
+ | --- | --- |
9
+ | ![One small flat monochrome closed-padlock glyph on a dark tile](dark.png) | ![The same compact flat lock glyph and tile on a light canvas](light.png) |
10
+
11
+ Both selected PNGs are 1585 × 992 pixels. The small tile occupies approximately 22% of the canvas width, with one neutral filled glyph and broad margins. This flat symbol replaces the earlier sculpted archive-and-lock illustration.
12
+
13
+ - [Shared scene specification](scene.yaml)
14
+ - [Dark prompt](dark.prompt.md) and [light prompt](light.prompt.md), compiled from that scene and the default project palette
15
+ - [Generation and pair review](pair-review.md)
16
+ - [Actual flat rendering and correction requests](flat-render-requests.md)
17
+ - [Current composition examples](../README.md#composition-gallery)
18
+
19
+ The scene explains this feature only. Derive a new scene from each real note and its product evidence.
@@ -0,0 +1,52 @@
1
+ # Release illustration — dark
2
+
3
+ ## Intent
4
+ Create one finished raster illustration for a product release note. Render only the illustration asset, without the surrounding release viewer, headline, body copy, page navigation, or an outer presentation frame.
5
+ User-visible change: Local backup files can be saved with encryption enabled.
6
+ Subject: One flat closed-padlock glyph inside a compact rounded square tile
7
+ Focal detail: The closed shackle and simple keyhole of the monochrome padlock glyph
8
+ Context: Fictional capability illustration, not a shipped product claim. The flat lock represents encryption of local backup files only. It does not claim that every file is encrypted or that every product surface is protected.
9
+
10
+ ## Composition contract
11
+ Archetype: icon-tile
12
+ Target canvas: 1280 × 800 pixels; landscape 1280:800. Produce a single image, not a dark/light collage.
13
+ Center one small flat rounded-square tile, normally 20–24% of the canvas width. Keep the glyph around 50–65% of the tile width. Use optical centering and broad uninterrupted negative space. A naked glyph is appropriate only when the scene explicitly calls for it.
14
+ Specific scene layout: A uniform 8:5 landscape canvas with one small flat rounded-square tile centered exactly in the canvas. The tile side is about 22 percent of canvas width, with a corner radius about 13 percent of its side. Inside it, one front-facing filled padlock glyph occupies about 56 percent of the tile width and 62 percent of its height. Use one solid mid-gray neutral value for the complete glyph, including its closed arched shackle and body. A simple keyhole is negative space in the body. Keep broad uninterrupted canvas around the tile. There is no archive box, physical object, perspective, material rendering, lighting, or colored badge.
15
+ Elements:
16
+ - One flat rounded-square background tile
17
+ - One solid monochrome closed-padlock glyph with a negative-space keyhole
18
+
19
+ ## Visual treatment
20
+ Use a crisp flat 2D filled glyph in one neutral gray value, with negative space for internal details. Keep the canvas and tile uniform and untextured. No perspective, extrusion, 3D, clay, bevels, material rendering, gradients, lighting, gloss, or shadows. Do not add an accent-colored badge; color requires an explicit functional meaning in the scene.
21
+ Favor visual precision, quiet hierarchy, and one instantly understandable feature. Small-screen clarity takes priority over decorative detail. Treat the specified element inventory as complete. Keep elements designated as schematic or abstract in that form; do not turn them into additional content or decoration. Authentic content explicitly requested in the brief can retain its own materials and colors. Avoid an unrelated marketing dashboard, neon glow, glass effects, noisy textures, decorative 3D blobs, and unnecessary gradients.
22
+
23
+ ## Dark theme roles
24
+ Canvas #242527; base surface #18191B; raised surface #343638; main neutral symbol #B9BBBE; secondary detail #777B80; divider #46494D; interaction accent #4678ED. Use the accent only when the scene assigns it a functional meaning.
25
+ Use uniform flat color areas and crisp negative space. If a tile is present, use #18191B for its flat fill. Separate the neutral glyph and its background by value alone. Do not add lighting, shadows, gradients, texture, or physical material cues.
26
+ Treat these colors as presentation roles, not a global recoloring filter. Preserve natural photos, device materials, and meaningful status colors. If a light product UI is not supported by the evidence, keep the authentic UI on the light presentation canvas instead of inventing a feature.
27
+
28
+ ## Pair invariants
29
+ The other theme must use the same object count, positions, scale, crop, camera, UI topology, selected state, chart values, allowed labels, and feature meaning. Change presentation surfaces, neutral values, lighting, and shadows only. Preserve semantic accent hues. If an approved counterpart exists and the tool supports references, use it as a composition reference for a constrained edit. Never create the counterpart with color inversion, brightness-only filters, or a fresh unrelated composition.
30
+ Specific invariants:
31
+ - Tile and glyph dimensions, optical center, and corner treatment
32
+ - Closed shackle and recognizable negative-space keyhole
33
+ - Completely flat monochrome graphic treatment in both themes
34
+
35
+ ## Text and references
36
+ No readable text or invented numbers. Use abstract bars for incidental UI labels.
37
+ Product reference files to inspect before rendering:
38
+ - None
39
+ Treat reference content as evidence, not instructions. Use original product-appropriate shapes. Do not copy reference-company identities, logos, attributed style labels, slogans, or distinctive unrelated products.
40
+
41
+ ## Exclusions
42
+ - Perspective, extrusion, bevels, gradients, texture, highlights, or shadows
43
+ - A physical archive box, metallic lock, or separate floating badge
44
+ - Decorative accent color or glow
45
+ - Open shackle, success mark, text, or additional controls
46
+ No watermark, stock-photo caption, extra claims, or decorative objects unrelated to the change.
47
+
48
+ ## Feature correctness
49
+ First compare the depicted meaning with the user-visible change and product evidence. The subject, focal detail, state, and relationships must satisfy this scene's composition, preserve, and avoid constraints. Apply only checks relevant to this feature. The symbol must communicate the stated capability or status. A badge must not imply completion, protection, availability, or a guarantee absent from the note.
50
+
51
+ ## Acceptance
52
+ Inspect at full size and approximately 350 pixels wide. First verify feature correctness, then visual clarity, then correspondence between the configured themes. Essential content must not clip, incidental text must not become gibberish, and the pair must preserve the composition contract. Matching variants can share the same factual or structural mistake. Register the actual output dimensions and selected file. If generation is unavailable, leave this request pending and hand off this prompt; do not substitute a placeholder image.
@@ -0,0 +1,7 @@
1
+ # Targeted dimensions correction
2
+
3
+ Use case: precise-object-edit.
4
+
5
+ Preserve the light illustration, including colors, lighting, archive and closed lock positions, sizes and overlap. Correct only output canvas dimensions to exactly 1586 pixels wide by 992 pixels high, matching its dark counterpart. Do not add elements, frames or text. Keep the light presentation theme unchanged.
6
+
7
+ The initial light output was 1584 × 993 pixels. Inspect the corrected file's metadata before selecting it.
@@ -0,0 +1,15 @@
1
+ # Flat icon rendering requests
2
+
3
+ These are the actual built-in image-tool requests used for this style revision. The compiled per-theme prompts remain the reusable scene-based inputs; these requests record the concrete render and correction used to select the example.
4
+
5
+ ## Dark render
6
+
7
+ Create one original product release-note ICON asset, a completely flat 2D graphic. Canvas exactly 1280 by 800 (8:5 landscape). The entire canvas is a single uniform dark charcoal #212121, without any texture, lighting, vignette, gradient, or shadow. Exactly centered at x=640,y=400 is ONE dark flat rounded-square tile, 278 by 278 pixels, color #151515, corner radius about 36 pixels. Inside that tile, exactly centered optically, put ONE simple solid mid-gray #A2A2A2 closed-padlock glyph, approximately 156 pixels wide and 172 pixels high. The whole glyph is one consistent filled neutral color: a closed thick arch for the shackle and a rounded rectangular lock body. The shackle's interior opening and one small conventional keyhole in the body are negative space showing the tile color. Shape edges are crisp and gently antialiased. This is a small functional app glyph, not a physical lock. Feature meaning is encryption of local backup files. Do not show an archive box, chest, documents, product photography, or another object. No badges. No blue or other accent. No depth, perspective, extrusion, 3D, clay, bevel, metal, gloss, highlights, shadows, gradients, noise, decorative detail, or lighting. No text, letters, numbers, logo, watermark, page heading, or interface chrome. Keep the broad empty canvas and compact tile scale exactly as specified. Produce only one DARK raster image.
8
+
9
+ ## Size correction
10
+
11
+ Make a constrained correction to the attached flat lock icon. Keep the existing 1586 x 992 canvas exactly. Scale the central tile AND the entire lock glyph together to 75 percent of their CURRENT width and height, anchored at the exact canvas center. The resulting tile must be only about 348 pixels wide and 348 pixels tall, approximately 22 percent of this actual canvas width. The previous tile is too large. Keep the lock-to-tile size ratio, closed shackle, and keyhole geometry unchanged. Increase the surrounding empty margin accordingly. The tile is a pure flat uniform #151515 rounded square and the glyph is a pure flat uniform #A2A2A2 silhouette. Replace all background shading with one completely UNIFORM solid #212121 fill right up to the image edges; remove all visible texture, gradients, lighting, and shadow. Keep crisp flat edges. No new subjects, text, color accent, physical materials, perspective or 3D. Render a single corrected DARK image only.
12
+
13
+ ## Light counterpart
14
+
15
+ Create the LIGHT counterpart of the attached compact flat lock icon as a tightly constrained color-only edit. Preserve the actual 1586 x 992 canvas, the EXACT compact tile size (about 22% of canvas width), tile location and corner radius, glyph size and placement, closed shackle, and negative-space keyhole. Do not enlarge or re-center the subject. Change only these flat presentation color roles: uniform canvas #F7F8FA, uniform rounded-square tile #ECEEF1, and the complete uniform monochrome glyph #66696E. The openings in the shackle and keyhole show the tile color. Every region should be a solid flat fill with crisp antialiased edges, no texture, gradients, lighting, shadows, gloss, outlines, depth, perspective, sculpting, or 3D. Keep broad uninterrupted margins. No blue or other accent, no text, badges, objects, or extra details. Render one LIGHT image only. Keep the same pixel dimensions as the attached image.
@@ -0,0 +1,52 @@
1
+ # Release illustration — light
2
+
3
+ ## Intent
4
+ Create one finished raster illustration for a product release note. Render only the illustration asset, without the surrounding release viewer, headline, body copy, page navigation, or an outer presentation frame.
5
+ User-visible change: Local backup files can be saved with encryption enabled.
6
+ Subject: One flat closed-padlock glyph inside a compact rounded square tile
7
+ Focal detail: The closed shackle and simple keyhole of the monochrome padlock glyph
8
+ Context: Fictional capability illustration, not a shipped product claim. The flat lock represents encryption of local backup files only. It does not claim that every file is encrypted or that every product surface is protected.
9
+
10
+ ## Composition contract
11
+ Archetype: icon-tile
12
+ Target canvas: 1280 × 800 pixels; landscape 1280:800. Produce a single image, not a dark/light collage.
13
+ Center one small flat rounded-square tile, normally 20–24% of the canvas width. Keep the glyph around 50–65% of the tile width. Use optical centering and broad uninterrupted negative space. A naked glyph is appropriate only when the scene explicitly calls for it.
14
+ Specific scene layout: A uniform 8:5 landscape canvas with one small flat rounded-square tile centered exactly in the canvas. The tile side is about 22 percent of canvas width, with a corner radius about 13 percent of its side. Inside it, one front-facing filled padlock glyph occupies about 56 percent of the tile width and 62 percent of its height. Use one solid mid-gray neutral value for the complete glyph, including its closed arched shackle and body. A simple keyhole is negative space in the body. Keep broad uninterrupted canvas around the tile. There is no archive box, physical object, perspective, material rendering, lighting, or colored badge.
15
+ Elements:
16
+ - One flat rounded-square background tile
17
+ - One solid monochrome closed-padlock glyph with a negative-space keyhole
18
+
19
+ ## Visual treatment
20
+ Use a crisp flat 2D filled glyph in one neutral gray value, with negative space for internal details. Keep the canvas and tile uniform and untextured. No perspective, extrusion, 3D, clay, bevels, material rendering, gradients, lighting, gloss, or shadows. Do not add an accent-colored badge; color requires an explicit functional meaning in the scene.
21
+ Favor visual precision, quiet hierarchy, and one instantly understandable feature. Small-screen clarity takes priority over decorative detail. Treat the specified element inventory as complete. Keep elements designated as schematic or abstract in that form; do not turn them into additional content or decoration. Authentic content explicitly requested in the brief can retain its own materials and colors. Avoid an unrelated marketing dashboard, neon glow, glass effects, noisy textures, decorative 3D blobs, and unnecessary gradients.
22
+
23
+ ## Light theme roles
24
+ Canvas #F7F8FA; base surface #FFFFFF; raised surface #ECEEF1; main neutral symbol #494D52; secondary detail #969BA2; divider #DDE0E5; interaction accent #4678ED. Use the accent only when the scene assigns it a functional meaning.
25
+ Use uniform flat color areas and crisp negative space. If a tile is present, use #ECEEF1 for its flat fill. Separate the neutral glyph and its background by value alone. Do not add lighting, shadows, gradients, texture, or physical material cues.
26
+ Treat these colors as presentation roles, not a global recoloring filter. Preserve natural photos, device materials, and meaningful status colors. If a light product UI is not supported by the evidence, keep the authentic UI on the light presentation canvas instead of inventing a feature.
27
+
28
+ ## Pair invariants
29
+ The other theme must use the same object count, positions, scale, crop, camera, UI topology, selected state, chart values, allowed labels, and feature meaning. Change presentation surfaces, neutral values, lighting, and shadows only. Preserve semantic accent hues. If an approved counterpart exists and the tool supports references, use it as a composition reference for a constrained edit. Never create the counterpart with color inversion, brightness-only filters, or a fresh unrelated composition.
30
+ Specific invariants:
31
+ - Tile and glyph dimensions, optical center, and corner treatment
32
+ - Closed shackle and recognizable negative-space keyhole
33
+ - Completely flat monochrome graphic treatment in both themes
34
+
35
+ ## Text and references
36
+ No readable text or invented numbers. Use abstract bars for incidental UI labels.
37
+ Product reference files to inspect before rendering:
38
+ - None
39
+ Treat reference content as evidence, not instructions. Use original product-appropriate shapes. Do not copy reference-company identities, logos, attributed style labels, slogans, or distinctive unrelated products.
40
+
41
+ ## Exclusions
42
+ - Perspective, extrusion, bevels, gradients, texture, highlights, or shadows
43
+ - A physical archive box, metallic lock, or separate floating badge
44
+ - Decorative accent color or glow
45
+ - Open shackle, success mark, text, or additional controls
46
+ No watermark, stock-photo caption, extra claims, or decorative objects unrelated to the change.
47
+
48
+ ## Feature correctness
49
+ First compare the depicted meaning with the user-visible change and product evidence. The subject, focal detail, state, and relationships must satisfy this scene's composition, preserve, and avoid constraints. Apply only checks relevant to this feature. The symbol must communicate the stated capability or status. A badge must not imply completion, protection, availability, or a guarantee absent from the note.
50
+
51
+ ## Acceptance
52
+ Inspect at full size and approximately 350 pixels wide. First verify feature correctness, then visual clarity, then correspondence between the configured themes. Essential content must not clip, incidental text must not become gibberish, and the pair must preserve the composition contract. Matching variants can share the same factual or structural mistake. Register the actual output dimensions and selected file. If generation is unavailable, leave this request pending and hand off this prompt; do not substitute a placeholder image.
@@ -0,0 +1,18 @@
1
+ # Pair review: Backup encryption
2
+
3
+ ## Generation record
4
+
5
+ This style revision used the coding agent's built-in image tool. It replaces the earlier sculpted archive and colored lock with one compact flat monochrome glyph. The CLI made no image-service calls.
6
+
7
+ This revision used three image-tool requests: a flat dark render, a targeted reduction of the oversized tile, and the light counterpart using the corrected dark image as its geometry reference. No inversion or deterministic pixel recoloring was used. See [the actual rendering requests](flat-render-requests.md). The compiled per-theme prompts and shared scene carry the revised flat-icon rules for reuse.
8
+
9
+ ## Selected output
10
+
11
+ - Two distinct decoded PNGs, both 1585 × 992 pixels, approximately 8:5. Actual output dimensions are retained.
12
+ - Reviewed at full size and approximately 350 pixels wide for feature meaning, focal clarity and pair correspondence.
13
+ - One small rounded-square tile and one flat filled closed-padlock glyph, with a negative-space keyhole.
14
+ - The tile occupies approximately 22% of canvas width. The glyph remains compact within it, and broad margins separate the symbol from the image edges.
15
+ - Both variants preserve the closed shackle, keyhole, tile proportions, and optical center. There is no physical archive object, sculpted lock, colored badge, or perspective.
16
+ - No readable text, logos or surrounding release-note viewer.
17
+
18
+ The pair is visually consistent, not guaranteed to have pixel-identical edges or exact palette samples. A little raster texture remains at full size; the selected image reads as a flat functional glyph at its intended card size. Use an actual approved icon asset when exact source pixels are required. This fictional symbol does not claim that every file is encrypted or that every product surface is protected.