@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.
- package/LICENSE +21 -21
- package/README.md +306 -110
- package/dist/cli.js +7 -5
- package/dist/content.d.ts +8 -0
- package/dist/content.js +5 -3
- package/dist/export.js +4 -3
- package/dist/git.d.ts +17 -6
- package/dist/git.js +9 -9
- package/dist/images.d.ts +14 -5
- package/dist/images.js +32 -8
- package/dist/model.d.ts +29 -19
- package/dist/model.js +27 -8
- package/dist/project.d.ts +1 -2
- package/dist/project.js +7 -12
- package/dist/prompts.d.ts +2 -2
- package/dist/prompts.js +24 -16
- package/dist/schema-export.js +9 -2
- package/dist/validate.js +10 -12
- package/examples/README.md +34 -14
- package/examples/backup-encryption/README.md +19 -0
- package/examples/backup-encryption/dark.png +0 -0
- package/examples/backup-encryption/dark.prompt.md +52 -0
- package/examples/backup-encryption/dimensions-edit.prompt.md +7 -0
- package/examples/backup-encryption/flat-render-requests.md +15 -0
- package/examples/backup-encryption/light.png +0 -0
- package/examples/backup-encryption/light.prompt.md +52 -0
- package/examples/backup-encryption/pair-review.md +18 -0
- package/examples/backup-encryption/scene.yaml +31 -0
- package/examples/connected-route/README.md +23 -0
- package/examples/connected-route/dark.png +0 -0
- package/examples/connected-route/dark.prompt.md +58 -0
- package/examples/connected-route/light.png +0 -0
- package/examples/connected-route/light.prompt.md +58 -0
- package/examples/connected-route/pair-review.md +22 -0
- package/examples/connected-route/render-requests.md +171 -0
- package/examples/connected-route/scene.yaml +59 -0
- package/examples/feature-briefs.yaml +96 -89
- package/examples/location-preferences/README.md +18 -0
- package/examples/location-preferences/dark.png +0 -0
- package/examples/location-preferences/dark.prompt.md +52 -0
- package/examples/location-preferences/light.png +0 -0
- package/examples/location-preferences/light.prompt.md +52 -0
- package/examples/location-preferences/pair-review.md +17 -0
- package/examples/location-preferences/scene.yaml +26 -0
- package/examples/provided-media/README.md +15 -0
- package/examples/provided-media/scene.yaml +22 -0
- package/examples/queue-action/README.md +15 -15
- package/examples/queue-action/alignment-edit.prompt.md +8 -8
- package/examples/queue-action/dark.prompt.md +58 -58
- package/examples/queue-action/light.prompt.md +58 -58
- package/examples/queue-action/pair-review.md +33 -33
- package/examples/queue-action/scene.yaml +41 -41
- package/examples/release-notes.en-US.json +56 -56
- package/examples/release-notes.ko-KR.json +56 -56
- package/examples/storage-breakdown/README.md +18 -0
- package/examples/storage-breakdown/dark.png +0 -0
- package/examples/storage-breakdown/dark.prompt.md +52 -0
- package/examples/storage-breakdown/light.png +0 -0
- package/examples/storage-breakdown/light.prompt.md +52 -0
- package/examples/storage-breakdown/pair-review.md +18 -0
- package/examples/storage-breakdown/scene.yaml +28 -0
- package/examples/tablet-reading/README.md +18 -0
- package/examples/tablet-reading/content-edit.prompt.md +7 -0
- package/examples/tablet-reading/dark.png +0 -0
- package/examples/tablet-reading/dark.prompt.md +52 -0
- package/examples/tablet-reading/light.png +0 -0
- package/examples/tablet-reading/light.prompt.md +52 -0
- package/examples/tablet-reading/pair-review.md +18 -0
- package/examples/tablet-reading/scene.yaml +29 -0
- package/kit/references/composition-recipes.md +89 -73
- package/kit/references/format.md +27 -24
- package/kit/references/media-sources.md +34 -0
- package/kit/references/theme-pairing.md +55 -53
- package/kit/references/visual-language.md +73 -69
- package/kit/references/workflow.md +90 -24
- package/kit/references/writing.md +27 -27
- package/kit/skills/releasekit-draft/SKILL.md +12 -10
- package/kit/skills/releasekit-image/SKILL.md +20 -16
- package/kit/skills/releasekit-review/SKILL.md +16 -12
- package/kit/skills/releasekit-translate/SKILL.md +14 -10
- package/package.json +54 -52
- package/schemas/bundle.schema.json +26 -1
- package/schemas/visual.schema.json +42 -0
- 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
|
|
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
|
-
|
|
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,
|
|
64
|
-
const boundary =
|
|
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
|
|
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
|
|
9
|
+
export type ImageRequest = {
|
|
10
10
|
note: string;
|
|
11
|
-
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:
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
4
|
-
import { exists, identifier, readYaml, within, writeYaml,
|
|
5
|
-
import { repoRoot,
|
|
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
|
|
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 ===
|
|
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,
|
|
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
|
|
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:
|
|
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
|
|
6
|
-
treatment: '
|
|
5
|
+
framing: '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.',
|
|
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
|
|
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
|
|
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: '
|
|
26
|
-
treatment: '
|
|
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: '
|
|
31
|
-
treatment: '
|
|
32
|
-
review: 'Check positions, connections, direction, scale relationships, and layer meanings against the scene. Routes must
|
|
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: '
|
|
41
|
-
treatment: '
|
|
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
|
-
(
|
|
62
|
-
?
|
|
63
|
-
:
|
|
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/schema-export.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
25
|
-
|
|
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
|
|
36
|
-
const changedPaths = new Set(evidence
|
|
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),
|
|
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));
|