@iodes/releasekit 0.1.0 → 0.1.2

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 (84) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +306 -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/git.d.ts +17 -6
  8. package/dist/git.js +9 -9
  9. package/dist/images.d.ts +14 -5
  10. package/dist/images.js +32 -8
  11. package/dist/model.d.ts +29 -19
  12. package/dist/model.js +27 -8
  13. package/dist/project.d.ts +1 -2
  14. package/dist/project.js +7 -12
  15. package/dist/prompts.d.ts +2 -2
  16. package/dist/prompts.js +24 -16
  17. package/dist/schema-export.js +9 -2
  18. package/dist/validate.js +10 -12
  19. package/examples/README.md +34 -14
  20. package/examples/backup-encryption/README.md +19 -0
  21. package/examples/backup-encryption/dark.png +0 -0
  22. package/examples/backup-encryption/dark.prompt.md +52 -0
  23. package/examples/backup-encryption/dimensions-edit.prompt.md +7 -0
  24. package/examples/backup-encryption/flat-render-requests.md +15 -0
  25. package/examples/backup-encryption/light.png +0 -0
  26. package/examples/backup-encryption/light.prompt.md +52 -0
  27. package/examples/backup-encryption/pair-review.md +18 -0
  28. package/examples/backup-encryption/scene.yaml +31 -0
  29. package/examples/connected-route/README.md +23 -0
  30. package/examples/connected-route/dark.png +0 -0
  31. package/examples/connected-route/dark.prompt.md +58 -0
  32. package/examples/connected-route/light.png +0 -0
  33. package/examples/connected-route/light.prompt.md +58 -0
  34. package/examples/connected-route/pair-review.md +22 -0
  35. package/examples/connected-route/render-requests.md +171 -0
  36. package/examples/connected-route/scene.yaml +59 -0
  37. package/examples/feature-briefs.yaml +96 -89
  38. package/examples/location-preferences/README.md +18 -0
  39. package/examples/location-preferences/dark.png +0 -0
  40. package/examples/location-preferences/dark.prompt.md +52 -0
  41. package/examples/location-preferences/light.png +0 -0
  42. package/examples/location-preferences/light.prompt.md +52 -0
  43. package/examples/location-preferences/pair-review.md +17 -0
  44. package/examples/location-preferences/scene.yaml +26 -0
  45. package/examples/provided-media/README.md +15 -0
  46. package/examples/provided-media/scene.yaml +22 -0
  47. package/examples/queue-action/README.md +15 -15
  48. package/examples/queue-action/alignment-edit.prompt.md +8 -8
  49. package/examples/queue-action/dark.prompt.md +58 -58
  50. package/examples/queue-action/light.prompt.md +58 -58
  51. package/examples/queue-action/pair-review.md +33 -33
  52. package/examples/queue-action/scene.yaml +41 -41
  53. package/examples/release-notes.en-US.json +56 -56
  54. package/examples/release-notes.ko-KR.json +56 -56
  55. package/examples/storage-breakdown/README.md +18 -0
  56. package/examples/storage-breakdown/dark.png +0 -0
  57. package/examples/storage-breakdown/dark.prompt.md +52 -0
  58. package/examples/storage-breakdown/light.png +0 -0
  59. package/examples/storage-breakdown/light.prompt.md +52 -0
  60. package/examples/storage-breakdown/pair-review.md +18 -0
  61. package/examples/storage-breakdown/scene.yaml +28 -0
  62. package/examples/tablet-reading/README.md +18 -0
  63. package/examples/tablet-reading/content-edit.prompt.md +7 -0
  64. package/examples/tablet-reading/dark.png +0 -0
  65. package/examples/tablet-reading/dark.prompt.md +52 -0
  66. package/examples/tablet-reading/light.png +0 -0
  67. package/examples/tablet-reading/light.prompt.md +52 -0
  68. package/examples/tablet-reading/pair-review.md +18 -0
  69. package/examples/tablet-reading/scene.yaml +29 -0
  70. package/kit/references/composition-recipes.md +89 -73
  71. package/kit/references/format.md +27 -24
  72. package/kit/references/media-sources.md +34 -0
  73. package/kit/references/theme-pairing.md +55 -53
  74. package/kit/references/visual-language.md +73 -69
  75. package/kit/references/workflow.md +90 -24
  76. package/kit/references/writing.md +27 -27
  77. package/kit/skills/releasekit-draft/SKILL.md +12 -10
  78. package/kit/skills/releasekit-image/SKILL.md +20 -16
  79. package/kit/skills/releasekit-review/SKILL.md +16 -12
  80. package/kit/skills/releasekit-translate/SKILL.md +14 -10
  81. package/package.json +54 -52
  82. package/schemas/bundle.schema.json +26 -1
  83. package/schemas/visual.schema.json +42 -0
  84. package/schemas/evidence.schema.json +0 -96
package/dist/git.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { spawnSync } from 'node:child_process';
2
- import { evidenceSchema } from './model.js';
3
2
  export function git(cwd, args, input) {
4
3
  const result = spawnSync('git', args, {
5
4
  cwd, input, encoding: 'utf8', windowsHide: true, maxBuffer: 64 * 1024 * 1024,
@@ -27,7 +26,7 @@ export function isAncestor(root, base, head) {
27
26
  return false;
28
27
  throw new Error(`Cannot establish Git ancestry: ${result.stderr || result.error?.message}`);
29
28
  }
30
- export function collect(root, from, to) {
29
+ export function resolveRange(root, from, to) {
31
30
  if (git(root, ['rev-parse', '--is-shallow-repository']).trim() === 'true') {
32
31
  throw new Error('Complete the shallow Git history before preparing a release; no fetch was performed.');
33
32
  }
@@ -38,6 +37,11 @@ export function collect(root, from, to) {
38
37
  if (fromSha && !isAncestor(root, fromSha, toSha)) {
39
38
  throw new Error('The start commit is not an ancestor of the end commit. Choose an explicit range on this release line.');
40
39
  }
40
+ return { fromRef: from, fromSha, toRef: to, toSha };
41
+ }
42
+ export function collect(root, from, to) {
43
+ const source = resolveRange(root, from, to);
44
+ const { fromSha, toSha } = source;
41
45
  const base = fromSha ?? git(root, ['hash-object', '-t', 'tree', '--stdin'], '').trim();
42
46
  const log = git(root, ['log', '--no-show-signature', '--format=%H%x00%s', fromSha ? `${fromSha}..${toSha}` : toSha, '--']);
43
47
  const commits = log.trimEnd() ? log.trimEnd().split('\n').map(line => {
@@ -54,14 +58,10 @@ export function collect(root, from, to) {
54
58
  else
55
59
  files.push({ status, path: first });
56
60
  }
57
- const patch = git(root, ['diff', '--no-ext-diff', '--no-textconv', '--find-renames', base, toSha, '--']);
58
- return {
59
- evidence: evidenceSchema.parse({ schemaVersion: 1, source: { fromRef: from, fromSha, toRef: to, toSha }, commits, files }),
60
- patch,
61
- };
61
+ return { source, commits, files };
62
62
  }
63
- export function checkPrevious(root, previous, evidence) {
64
- const boundary = evidence.source.fromSha;
63
+ export function checkPrevious(root, previous, source) {
64
+ const boundary = source.fromSha;
65
65
  if (!boundary || !isAncestor(root, previous.source.toSha, boundary)) {
66
66
  throw new Error('The previous release is not an ancestor of this comparison start. Choose the correct previous release.');
67
67
  }
package/dist/images.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Theme, type Visual } from './model.js';
1
+ import { type AssetVariant, type Visual } from './model.js';
2
2
  import { Project } from './project.js';
3
3
  export declare function inspectImage(bytes: Buffer): Promise<{
4
4
  width: number;
@@ -6,13 +6,20 @@ export declare function inspectImage(bytes: Buffer): Promise<{
6
6
  extension: string;
7
7
  sha256: string;
8
8
  }>;
9
- export interface ImageRequest {
9
+ export type ImageRequest = {
10
10
  note: string;
11
- theme: Theme;
11
+ theme: AssetVariant;
12
12
  reason: 'missing' | 'stale';
13
+ } & ({
14
+ action: 'generate';
13
15
  promptFile: string;
14
16
  compositionReference: string | null;
15
- }
17
+ } | {
18
+ action: 'provide';
19
+ promptFile: null;
20
+ compositionReference: null;
21
+ instruction: string;
22
+ });
16
23
  export declare function planImages(project: Project, version: string): Promise<{
17
24
  version: string;
18
25
  configuredThemes: ("dark" | "light")[];
@@ -20,9 +27,11 @@ export declare function planImages(project: Project, version: string): Promise<{
20
27
  readyAssets: number;
21
28
  pendingAssets: number;
22
29
  requests: ImageRequest[];
30
+ generationRequests: number;
31
+ providedRequests: number;
23
32
  costNote: string;
24
33
  }>;
25
- export declare function importImage(project: Project, version: string, noteId: string, variant: Theme, source: string): Promise<{
34
+ export declare function importImage(project: Project, version: string, noteId: string, variant: AssetVariant, source: string): Promise<{
26
35
  file: string;
27
36
  sha256: string;
28
37
  sceneHash: string;
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,28 +265,16 @@ 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>;
257
- export declare const evidenceSchema: z.ZodObject<{
258
- schemaVersion: z.ZodLiteral<1>;
259
- source: z.ZodObject<{
260
- fromRef: z.ZodNullable<z.ZodString>;
261
- fromSha: z.ZodNullable<z.ZodString>;
262
- toRef: z.ZodString;
263
- toSha: z.ZodString;
264
- }, z.core.$strict>;
265
- commits: z.ZodArray<z.ZodObject<{
266
- sha: z.ZodString;
267
- subject: z.ZodString;
268
- }, z.core.$strict>>;
269
- files: z.ZodArray<z.ZodObject<{
270
- status: z.ZodString;
271
- path: z.ZodString;
272
- oldPath: z.ZodOptional<z.ZodString>;
273
- }, z.core.$strict>>;
274
- }, z.core.$strict>;
275
- export type Evidence = z.infer<typeof evidenceSchema>;
276
278
  export declare const bundleSchema: z.ZodObject<{
277
279
  schemaVersion: z.ZodLiteral<1>;
278
280
  currentVersion: z.ZodString;
@@ -296,6 +298,7 @@ export declare const bundleSchema: z.ZodObject<{
296
298
  fallbackTheme: z.ZodEnum<{
297
299
  dark: "dark";
298
300
  light: "light";
301
+ shared: "shared";
299
302
  }>;
300
303
  variants: z.ZodObject<{
301
304
  dark: z.ZodOptional<z.ZodObject<{
@@ -308,6 +311,11 @@ export declare const bundleSchema: z.ZodObject<{
308
311
  width: z.ZodNumber;
309
312
  height: z.ZodNumber;
310
313
  }, z.core.$strict>>;
314
+ shared: z.ZodOptional<z.ZodObject<{
315
+ src: z.ZodString;
316
+ width: z.ZodNumber;
317
+ height: z.ZodNumber;
318
+ }, z.core.$strict>>;
311
319
  }, z.core.$strict>;
312
320
  }, z.core.$strict>>;
313
321
  }, z.core.$strict>>;
@@ -315,4 +323,6 @@ export declare const bundleSchema: z.ZodObject<{
315
323
  }, z.core.$strict>;
316
324
  export type Bundle = z.infer<typeof bundleSchema>;
317
325
  export declare function themes(policy: VisualPolicy): Theme[];
326
+ export declare function imageSource(scene: Scene): 'generated' | 'provided';
327
+ export declare function activeVariants(visual: Visual, policy: VisualPolicy): AssetVariant[];
318
328
  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,12 +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() }),
64
- });
65
- export const evidenceSchema = z.strictObject({
66
- schemaVersion: z.literal(1), source: sourceSchema,
67
- commits: z.array(z.strictObject({ sha, subject: z.string() })),
68
- files: z.array(z.strictObject({ status: z.string(), path: z.string(), oldPath: z.string().optional() })),
65
+ variants: z.strictObject({ dark: assetSchema.optional(), light: assetSchema.optional(), shared: assetSchema.optional() }),
69
66
  });
70
67
  const exportedImage = z.strictObject({
71
68
  src: z.string(), width: z.number().int().positive(), height: z.number().int().positive(),
@@ -77,8 +74,8 @@ export const bundleSchema = z.strictObject({
77
74
  notes: z.array(z.strictObject({
78
75
  id: segment, category: noteMetaSchema.shape.category, title: z.string(), bodyMarkdown: z.string(),
79
76
  image: z.strictObject({
80
- alt: z.string(), fallbackTheme: theme,
81
- variants: z.strictObject({ dark: exportedImage.optional(), light: exportedImage.optional() }),
77
+ alt: z.string(), fallbackTheme: assetVariant,
78
+ variants: z.strictObject({ dark: exportedImage.optional(), light: exportedImage.optional(), shared: exportedImage.optional() }),
82
79
  }).nullable(),
83
80
  })),
84
81
  })),
@@ -86,6 +83,28 @@ export const bundleSchema = z.strictObject({
86
83
  export function themes(policy) {
87
84
  return policy.themes === 'both' ? ['dark', 'light'] : [policy.themes];
88
85
  }
86
+ export function imageSource(scene) {
87
+ if (scene.archetype === 'object-detail' || scene.archetype === 'editorial-scene') {
88
+ if (scene.source === 'generated')
89
+ throw new Error(`${scene.archetype} requires a supplied capture, photograph, or content image. Set source to provided.`);
90
+ return 'provided';
91
+ }
92
+ return scene.source ?? 'generated';
93
+ }
94
+ export function activeVariants(visual, policy) {
95
+ const provided = imageSource(visual.scene) === 'provided';
96
+ const { dark, light, shared } = visual.variants;
97
+ if (shared) {
98
+ if (!provided)
99
+ throw new Error('Only supplied images can use a shared asset.');
100
+ if (dark || light)
101
+ throw new Error('Choose a shared supplied image or distinct theme variants; do not mix both.');
102
+ return ['shared'];
103
+ }
104
+ if (provided && !dark && !light)
105
+ return ['shared'];
106
+ return themes(policy);
107
+ }
89
108
  export function defaultConfig(product) {
90
109
  return {
91
110
  schemaVersion: 1, product, sourceLocale: 'ko-KR', locales: ['ko-KR', 'en-US'],
package/dist/project.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type ProjectConfig, type Release, type Evidence } from './model.js';
1
+ import { type ProjectConfig, type Release } from './model.js';
2
2
  export declare class Project {
3
3
  readonly root: string;
4
4
  constructor(root: string);
@@ -10,7 +10,6 @@ export declare class Project {
10
10
  release(version: string): Promise<Release>;
11
11
  save(release: Release): Promise<void>;
12
12
  versions(): Promise<string[]>;
13
- evidence(version: string): Promise<Evidence>;
14
13
  history(version: string, limit: number): Promise<Release[]>;
15
14
  }
16
15
  export declare function editable(release: Release): void;
package/dist/project.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { configSchema, releaseSchema, evidenceSchema } from './model.js';
4
- import { exists, identifier, readYaml, within, writeYaml, write, KIT_DIR } from './files.js';
5
- import { repoRoot, collect, checkPrevious } from './git.js';
3
+ import { configSchema, releaseSchema } from './model.js';
4
+ import { exists, identifier, readYaml, within, writeYaml, KIT_DIR } from './files.js';
5
+ import { repoRoot, resolveRange, checkPrevious } from './git.js';
6
6
  export class Project {
7
7
  root;
8
8
  constructor(root) { this.root = path.resolve(root); }
@@ -37,9 +37,6 @@ export class Project {
37
37
  const entries = await fs.readdir(folder, { withFileTypes: true });
38
38
  return entries.filter(e => e.isDirectory()).map(e => e.name).sort();
39
39
  }
40
- async evidence(version) {
41
- return evidenceSchema.parse(JSON.parse(await fs.readFile(await this.releaseFile(version, 'evidence.json'), 'utf8')));
42
- }
43
40
  async history(version, limit) {
44
41
  if (!Number.isInteger(limit) || limit < 1 || limit > 100)
45
42
  throw new Error('History limit must be an integer from 1 to 100.');
@@ -77,27 +74,25 @@ export async function prepare(project, version, options) {
77
74
  const from = options.fromRoot ? null : options.from ?? previous?.source.toSha;
78
75
  if (from === undefined)
79
76
  throw new Error('Specify --from, --previous, or --from-root.');
80
- const { evidence, patch } = collect(project.root, from, options.to ?? 'HEAD');
77
+ const source = resolveRange(project.root, from, options.to ?? 'HEAD');
81
78
  if (!previous && !options.fromRoot && !options.firstRelease) {
82
79
  const existing = await Promise.all((await project.versions()).map(v => project.release(v)));
83
- const candidates = existing.filter(r => r.source.toSha === evidence.source.fromSha);
80
+ const candidates = existing.filter(r => r.source.toSha === source.fromSha);
84
81
  if (candidates.length === 1)
85
82
  previous = candidates[0];
86
83
  else if (existing.length)
87
84
  throw new Error('Previous release is ambiguous. Specify --previous, or --first-release for an independent release line.');
88
85
  }
89
86
  if (previous) {
90
- checkPrevious(project.root, previous, evidence);
87
+ checkPrevious(project.root, previous, source);
91
88
  await project.history(previous.version, 1);
92
89
  }
93
90
  const release = releaseSchema.parse({
94
91
  schemaVersion: 1, version, releasedAt: options.date ?? new Date().toISOString().slice(0, 10),
95
- previous: previous?.version ?? null, status: 'draft', source: evidence.source,
92
+ previous: previous?.version ?? null, status: 'draft', source,
96
93
  sourceLocale: config.sourceLocale, locales: config.locales, visuals: config.visuals,
97
94
  notes: [], emptyReason: null, contentHash: null,
98
95
  });
99
96
  await project.save(release);
100
- await write(await project.releaseFile(version, 'evidence.json'), JSON.stringify(evidence, null, 2) + '\n');
101
- await write(await project.releaseFile(version, 'changes.patch'), patch);
102
97
  return release;
103
98
  }
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` +
@@ -1,10 +1,17 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import { fileURLToPath } from 'node:url';
3
3
  import { z } from 'zod';
4
- import { configSchema, releaseSchema, visualSchema, noteTextSchema, evidenceSchema, bundleSchema } from './model.js';
4
+ import { configSchema, releaseSchema, visualSchema, noteTextSchema, bundleSchema } from './model.js';
5
5
  const directory = fileURLToPath(new URL('../schemas/', import.meta.url));
6
6
  await fs.mkdir(directory, { recursive: true });
7
- for (const [name, schema] of Object.entries({ config: configSchema, release: releaseSchema, visual: visualSchema, note: noteTextSchema, evidence: evidenceSchema, bundle: bundleSchema })) {
7
+ const schemas = { config: configSchema, release: releaseSchema, visual: visualSchema, note: noteTextSchema, bundle: bundleSchema };
8
+ for (const [name, schema] of Object.entries(schemas)) {
8
9
  const json = { ...z.toJSONSchema(schema), $id: `urn:releasekit:${name}:1` };
9
10
  await fs.writeFile(`${directory}/${name}.schema.json`, JSON.stringify(json, null, 2) + '\n');
10
11
  }
12
+ // Keep the generated directory aligned with the schemas the package exports.
13
+ const outputs = new Set(Object.keys(schemas).map(name => `${name}.schema.json`));
14
+ for (const file of await fs.readdir(directory)) {
15
+ if (file.endsWith('.schema.json') && !outputs.has(file))
16
+ await fs.unlink(`${directory}/${file}`);
17
+ }
package/dist/validate.js CHANGED
@@ -1,13 +1,12 @@
1
- import * as fs from 'node:fs/promises';
2
- import {} from './model.js';
1
+ import { imageSource } from './model.js';
3
2
  import { Project, editable } from './project.js';
4
3
  import { canonical, digest, readNote, noteHash, identifier } from './files.js';
5
4
  import { readVisual, checkReferenceFiles } from './content.js';
6
5
  import { validateImages } from './images.js';
7
- import { checkPrevious } from './git.js';
6
+ import { checkPrevious, collect } from './git.js';
8
7
  export async function contentHash(project, release) {
9
8
  const { status: _status, contentHash: _hash, ...metadata } = release;
10
- const parts = [metadata, await project.evidence(release.version), digest(await fs.readFile(await project.releaseFile(release.version, 'changes.patch')))];
9
+ const parts = [metadata];
11
10
  for (const note of release.notes) {
12
11
  for (const language of release.locales)
13
12
  parts.push(await readNote(await project.releaseFile(release.version, `notes/${note.id}/${language}.md`)));
@@ -21,9 +20,8 @@ export async function validate(project, version) {
21
20
  let hash = null;
22
21
  try {
23
22
  const release = await project.release(version);
24
- const evidence = await project.evidence(version);
25
- if (canonical(evidence.source) !== canonical(release.source))
26
- errors.push('Evidence and release Git boundaries disagree.');
23
+ // Drafts inspect the pinned Git range. Ready releases rely on their finalized fingerprint.
24
+ const evidence = release.status === 'draft' ? collect(project.root, release.source.fromSha, release.source.toSha) : null;
27
25
  if (!release.locales.includes(release.sourceLocale) || new Set(release.locales).size !== release.locales.length)
28
26
  errors.push('Release locales must be unique and include the source locale.');
29
27
  if (new Set(release.notes.map(n => n.id)).size !== release.notes.length)
@@ -32,13 +30,13 @@ export async function validate(project, version) {
32
30
  errors.push('No notes yet. Write the notes or explain the absence of user-visible changes in emptyReason.');
33
31
  if (release.notes.length && release.emptyReason !== null)
34
32
  errors.push('emptyReason must be null when notes are present.');
35
- const commits = new Set(evidence.commits.map(c => c.sha));
36
- const changedPaths = new Set(evidence.files.flatMap(f => [f.path, ...(f.oldPath ? [f.oldPath] : [])]));
33
+ const commits = new Set(evidence?.commits.map(c => c.sha));
34
+ const changedPaths = new Set(evidence?.files.flatMap(f => [f.path, ...(f.oldPath ? [f.oldPath] : [])]));
37
35
  for (const note of release.notes) {
38
36
  identifier(note.id);
39
37
  if (!note.commits.length && !note.paths.length)
40
38
  errors.push(`${note.id}: attach at least one changed path or commit as evidence.`);
41
- if (note.commits.some(c => !commits.has(c)) || note.paths.some(p => !changedPaths.has(p)))
39
+ if (evidence && (note.commits.some(c => !commits.has(c)) || note.paths.some(p => !changedPaths.has(p))))
42
40
  errors.push(`${note.id}: evidence points outside the prepared Git range.`);
43
41
  try {
44
42
  const source = await readNote(await project.releaseFile(version, `notes/${note.id}/${release.sourceLocale}.md`));
@@ -60,7 +58,7 @@ export async function validate(project, version) {
60
58
  if (note.image) {
61
59
  try {
62
60
  const visual = await readVisual(project, version, note.id);
63
- if (release.status === 'draft')
61
+ if (release.status === 'draft' && imageSource(visual.scene) === 'generated')
64
62
  await checkReferenceFiles(project, visual.scene.references);
65
63
  await validateImages(project, version, note.id, visual, errors, warnings);
66
64
  }
@@ -77,7 +75,7 @@ export async function validate(project, version) {
77
75
  }
78
76
  if (release.status === 'draft' && release.previous) {
79
77
  try {
80
- checkPrevious(project.root, await project.release(release.previous), evidence);
78
+ checkPrevious(project.root, await project.release(release.previous), release.source);
81
79
  }
82
80
  catch (error) {
83
81
  errors.push(error instanceof Error ? error.message : String(error));