@ankhorage/devtools 1.10.3 → 1.10.4
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/dist/tools/skills/assets/zora-designer/SKILL.md +0 -13
- package/dist/tools/skills/assets/zora-designer/references/artifact.md +1 -7
- package/dist/tools/skills/assets/zora-designer/references/workflow.md +7 -21
- package/dist/tools/skills/assets/zora-designer/scripts/audit.mjs +0 -6
- package/dist/tools/skills/assets/zora-designer/scripts/owner-api.mjs +1 -2
- package/dist/tools/skills/assets/zora-designer/scripts/scaffold-template.mjs +6 -39
- package/package.json +1 -1
- package/dist/tools/skills/assets/zora-designer/scripts/template-imagery.mjs +0 -361
- package/dist/tools/skills/zoraDesignerImagery.fixtures.d.ts +0 -14
- package/dist/tools/skills/zoraDesignerImagery.fixtures.js +0 -141
|
@@ -77,19 +77,6 @@ When a required capability or source is unavailable, report that deliverable or
|
|
|
77
77
|
blocked. Do not simulate an image, browser observation, or runtime proof. Clearly distinguish
|
|
78
78
|
generated concept images from authoritative runtime captures.
|
|
79
79
|
|
|
80
|
-
## Persist imagery before template release
|
|
81
|
-
|
|
82
|
-
For a finalized template, retain three distinct image inventories: ordered generated concept
|
|
83
|
-
screens, ordered authoritative runtime captures, and the original runtime images used by final
|
|
84
|
-
ZORA `Image` nodes. A screenshot never substitutes for its underlying hero, card, avatar, or other
|
|
85
|
-
runtime asset.
|
|
86
|
-
|
|
87
|
-
When scaffolding in Templates, provide these inventories through the normal scaffold input. The
|
|
88
|
-
scaffold copies durable source files into the template asset tree, registers each runtime image once
|
|
89
|
-
in the canonical manifest media pool, validates the resulting bundle through released Templates,
|
|
90
|
-
and writes the inventory to `zora-designer.md`. Read [workflow.md](references/workflow.md) for the
|
|
91
|
-
input contract and release checks.
|
|
92
|
-
|
|
93
80
|
## Validate before handoff
|
|
94
81
|
|
|
95
82
|
- Compile the category design and both theme modes through installed owner APIs.
|
|
@@ -31,9 +31,6 @@ is never invented:
|
|
|
31
31
|
- `tokens`: current owner-computed token output, never copied token definitions;
|
|
32
32
|
- `components`: metadata-backed recipe decisions and required states;
|
|
33
33
|
- `screens`: ordered screen specifications and evidence relationships;
|
|
34
|
-
- `imagery`: separate ordered `conceptSeries`, ordered `runtimeCaptures`, and `runtimeAssets` with
|
|
35
|
-
their durable template source path, generated-project target path where applicable, media ID,
|
|
36
|
-
dimensions, content type, byte size, origin, and provenance;
|
|
37
34
|
- `validation`: scope, gates, application gate, owner/runtime drift, and blockers;
|
|
38
35
|
- `auditInput`: criterion and release-gate assessments consumed by the canonical calculator;
|
|
39
36
|
- `findings`, `risks`, `openDecisions`, and preserved `userNotes`.
|
|
@@ -82,10 +79,7 @@ score impact, risks, passed rules, and not-assessable criteria. Keep unavailable
|
|
|
82
79
|
|
|
83
80
|
An image or series records each original source separately with dimensions and order. A URL records
|
|
84
81
|
the capture viewport, theme mode, state, and reproduction. Concept images are labeled generated;
|
|
85
|
-
runtime captures are labeled observed/measured and include their capture source.
|
|
86
|
-
assets additionally record the manifest media ID, checked-in Template source path, and
|
|
87
|
-
generated-project path. These are distinct inventories: a concept or runtime-capture screenshot
|
|
88
|
-
does not prove the underlying screen image asset was retained.
|
|
82
|
+
runtime captures are labeled observed/measured and include their capture source.
|
|
89
83
|
|
|
90
84
|
## Output and persistence
|
|
91
85
|
|
|
@@ -88,9 +88,7 @@ selected/focus states. Define narrow and wide behavior where applicable.
|
|
|
88
88
|
For a series also define the shared shell, canonical route topology, state continuity, back/cancel
|
|
89
89
|
behavior, shared recipes and terminology, and one common theme. A concept image series must remain
|
|
90
90
|
ordered and visually coherent. Label every generated image as a concept; replace it with an actual
|
|
91
|
-
runtime capture only after running the manifest.
|
|
92
|
-
files for every concept image, runtime capture, and final-screen runtime image with their measured
|
|
93
|
-
dimensions, mode/state/order, origin, and provenance.
|
|
91
|
+
runtime capture only after running the manifest.
|
|
94
92
|
|
|
95
93
|
## 5. Audit a URL, image, or series
|
|
96
94
|
|
|
@@ -119,24 +117,12 @@ When the target repository is `@ankhorage/templates`, scaffold reviewed composit
|
|
|
119
117
|
bun .agents/skills/zora-designer/scripts/scaffold-template.mjs scaffold-input.json
|
|
120
118
|
```
|
|
121
119
|
|
|
122
|
-
The scaffold input provides the ready manifest, category, template ID, label, description,
|
|
123
|
-
root
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
Each `imagery.conceptSeries` and `imagery.runtimeCaptures` entry supplies `id`, `sourcePath`,
|
|
129
|
-
`contentType`, measured `width`/`height`, `mode`, `state`, `order`, `origin`, and `provenance`.
|
|
130
|
-
Each `imagery.runtimeAssets` entry supplies `mediaId`, `name`, `sourcePath`, `contentType`, measured
|
|
131
|
-
`width`/`height`, `origin`, and `provenance`. The helper preserves source bytes under deterministic
|
|
132
|
-
template evidence/runtime paths, derives collision-safe generated-project paths, creates the
|
|
133
|
-
canonical bundled media entries, and validates the complete asset bundle through the released
|
|
134
|
-
Templates API. Supply only durable local source files: `blob:` and `data:` URLs, symlinks, missing
|
|
135
|
-
files, unsupported image formats, incomplete metadata, and duplicate identities are blocked.
|
|
136
|
-
|
|
137
|
-
The helper refuses existing targets, unsafe identifiers, blocked manifests, and non-Templates
|
|
138
|
-
repositories. Review and validate the generated production diff; do not hand-hide a systemic skill
|
|
139
|
-
or owner defect.
|
|
120
|
+
The scaffold input provides the ready manifest, category, template ID, label, description, and
|
|
121
|
+
target root. The helper verifies the Templates repository and owner validation, creates a normal
|
|
122
|
+
variant `manifest.ts`, `template.ts`, and `index.ts`, and updates the category registry import and
|
|
123
|
+
definition deterministically. It refuses existing targets, unsafe identifiers, blocked manifests,
|
|
124
|
+
and non-Templates repositories. Review and validate the generated production diff; do not hand-hide
|
|
125
|
+
a systemic skill or owner defect.
|
|
140
126
|
|
|
141
127
|
## 7. Delivery gates
|
|
142
128
|
|
|
@@ -12,7 +12,6 @@ const HUMAN_SECTIONS = [
|
|
|
12
12
|
'Layout, shape, elevation, and motion',
|
|
13
13
|
'Component and interaction states',
|
|
14
14
|
'Screen specifications',
|
|
15
|
-
'Visual assets',
|
|
16
15
|
'Accessibility and validation',
|
|
17
16
|
'Audit summary',
|
|
18
17
|
'Findings and remediation',
|
|
@@ -112,11 +111,6 @@ export function serializeArtifact(input, audit) {
|
|
|
112
111
|
tokens: input.tokens ?? {},
|
|
113
112
|
components: input.components ?? { stateRequirements: [], recipeDecisions: {} },
|
|
114
113
|
screens: input.screens ?? [],
|
|
115
|
-
imagery: input.imagery ?? {
|
|
116
|
-
conceptSeries: [],
|
|
117
|
-
runtimeCaptures: [],
|
|
118
|
-
runtimeAssets: [],
|
|
119
|
-
},
|
|
120
114
|
validation: input.validation ?? {
|
|
121
115
|
scope: documentKind === 'audit' ? 'audit' : 'configuration',
|
|
122
116
|
status: 'not-run',
|
|
@@ -5,7 +5,7 @@ import { dirname, join, parse, resolve } from 'node:path';
|
|
|
5
5
|
import { pathToFileURL } from 'node:url';
|
|
6
6
|
|
|
7
7
|
const OWNER_RELEASES = {
|
|
8
|
-
templates: { packageName: '@ankhorage/templates', minimumVersion: '
|
|
8
|
+
templates: { packageName: '@ankhorage/templates', minimumVersion: '8.0.0' },
|
|
9
9
|
zora: { packageName: '@ankhorage/zora', minimumVersion: '4.0.0' },
|
|
10
10
|
};
|
|
11
11
|
|
|
@@ -22,7 +22,6 @@ const OWNER_REQUIREMENTS = {
|
|
|
22
22
|
'composeCategoryAppManifest',
|
|
23
23
|
'validateTemplateManifest',
|
|
24
24
|
'assertTemplateManifestReady',
|
|
25
|
-
'createStarterTemplateArtifact',
|
|
26
25
|
],
|
|
27
26
|
},
|
|
28
27
|
zoraTheme: {
|
|
@@ -4,9 +4,7 @@ import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
|
4
4
|
import { join, relative, resolve, sep } from 'node:path';
|
|
5
5
|
import { pathToFileURL } from 'node:url';
|
|
6
6
|
|
|
7
|
-
import { serializeArtifact } from './audit.mjs';
|
|
8
7
|
import { loadOwnerApis } from './owner-api.mjs';
|
|
9
|
-
import { prepareTemplateImagery, writeTemplateImagery } from './template-imagery.mjs';
|
|
10
8
|
|
|
11
9
|
/*** Validate and scaffold one ready authored manifest into the normal Templates variant layout. */
|
|
12
10
|
export async function scaffoldTemplate(input) {
|
|
@@ -56,57 +54,38 @@ export async function scaffoldTemplate(input) {
|
|
|
56
54
|
);
|
|
57
55
|
}
|
|
58
56
|
|
|
59
|
-
const imagery = await prepareTemplateImagery(input, targetDirectory, manifest, owners);
|
|
60
57
|
const registryPath = join(categoryDirectory, 'index.ts');
|
|
61
58
|
const registrySource = await readFile(registryPath, 'utf8');
|
|
62
59
|
const symbol = toPascalCase(input.templateId);
|
|
63
60
|
const factoryBase = symbol.endsWith('Starter') ? symbol.slice(0, -'Starter'.length) : symbol;
|
|
64
61
|
const factoryName = `create${factoryBase}StarterTemplate`;
|
|
65
62
|
const manifestName = `AUTHORED_${toConstantCase(input.templateId)}_MANIFEST`;
|
|
66
|
-
const assetsName = `AUTHORED_${toConstantCase(input.templateId)}_ASSETS`;
|
|
67
63
|
const registrySourceUpdated = updateCategoryRegistry(registrySource, {
|
|
68
64
|
templateId: input.templateId,
|
|
69
65
|
label: input.label,
|
|
70
66
|
description: input.description,
|
|
71
67
|
factoryName,
|
|
72
|
-
assetsName,
|
|
73
68
|
});
|
|
74
|
-
const files = createTemplateFiles({
|
|
75
|
-
manifest: imagery.artifact.manifest,
|
|
76
|
-
manifestName,
|
|
77
|
-
factoryName,
|
|
78
|
-
assets: imagery.artifact.assets,
|
|
79
|
-
assetsName,
|
|
80
|
-
});
|
|
81
|
-
const artifactPath = resolve(targetDirectory, input.artifactPath ?? 'zora-designer.md');
|
|
82
|
-
assertInside(targetDirectory, artifactPath);
|
|
83
|
-
const artifact = serializeArtifact({ ...(input.artifact ?? {}), imagery: imagery.inventory });
|
|
69
|
+
const files = createTemplateFiles({ manifest, manifestName, factoryName });
|
|
84
70
|
|
|
85
71
|
await mkdir(variantDirectory, { recursive: true });
|
|
86
72
|
for (const [fileName, contents] of Object.entries(files)) {
|
|
87
73
|
await writeFile(join(variantDirectory, fileName), contents);
|
|
88
74
|
}
|
|
89
|
-
await writeTemplateImagery(imagery.files, targetDirectory, writeFile, mkdir);
|
|
90
75
|
await writeFile(registryPath, registrySourceUpdated);
|
|
91
|
-
await writeFile(artifactPath, artifact);
|
|
92
76
|
|
|
93
77
|
return {
|
|
94
78
|
targetDirectory,
|
|
95
79
|
registryPath: relative(targetDirectory, registryPath),
|
|
96
|
-
createdFiles:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
),
|
|
100
|
-
...imagery.files.map((file) => file.destinationPath),
|
|
101
|
-
relative(targetDirectory, artifactPath),
|
|
102
|
-
].sort(),
|
|
80
|
+
createdFiles: Object.keys(files).map((fileName) =>
|
|
81
|
+
relative(targetDirectory, join(variantDirectory, fileName)),
|
|
82
|
+
),
|
|
103
83
|
factoryName,
|
|
104
|
-
imagery: imagery.inventory,
|
|
105
84
|
};
|
|
106
85
|
}
|
|
107
86
|
|
|
108
87
|
/*** Create normal manifest, factory, and entrypoint source for one authored starter variant. */
|
|
109
|
-
function createTemplateFiles({ manifest, manifestName, factoryName
|
|
88
|
+
function createTemplateFiles({ manifest, manifestName, factoryName }) {
|
|
110
89
|
const manifestSource = `import type { AppManifest } from '@ankhorage/contracts';
|
|
111
90
|
|
|
112
91
|
export const ${manifestName} = ${JSON.stringify(manifest, null, 2)} satisfies AppManifest;
|
|
@@ -135,13 +114,8 @@ export function ${factoryName}(seed: TemplateSeed): AppManifest {
|
|
|
135
114
|
activeThemeId: theme.id,
|
|
136
115
|
};
|
|
137
116
|
}
|
|
138
|
-
`;
|
|
139
|
-
const assetSource = `import type { StarterTemplateAsset } from '../../../starter.assets';
|
|
140
|
-
|
|
141
|
-
export const ${assetsName} = ${JSON.stringify(assets, null, 2)} satisfies readonly StarterTemplateAsset[];
|
|
142
117
|
`;
|
|
143
118
|
return {
|
|
144
|
-
'assets.ts': assetSource,
|
|
145
119
|
'index.ts': `export { ${factoryName} } from './template';\n`,
|
|
146
120
|
'manifest.ts': manifestSource,
|
|
147
121
|
'template.ts': templateSource,
|
|
@@ -151,12 +125,7 @@ export const ${assetsName} = ${JSON.stringify(assets, null, 2)} satisfies readon
|
|
|
151
125
|
/*** Add one stable import and definition to an existing category registry. */
|
|
152
126
|
function updateCategoryRegistry(source, definition) {
|
|
153
127
|
const importLine = `import { ${definition.factoryName} } from './${definition.templateId}';`;
|
|
154
|
-
|
|
155
|
-
if (
|
|
156
|
-
source.includes(`id: '${definition.templateId}'`) ||
|
|
157
|
-
source.includes(importLine) ||
|
|
158
|
-
source.includes(assetsImportLine)
|
|
159
|
-
) {
|
|
128
|
+
if (source.includes(`id: '${definition.templateId}'`) || source.includes(importLine)) {
|
|
160
129
|
throw new Error(`Template is already registered: ${definition.templateId}`);
|
|
161
130
|
}
|
|
162
131
|
const exportMarker = '\nexport const ';
|
|
@@ -168,7 +137,6 @@ function updateCategoryRegistry(source, definition) {
|
|
|
168
137
|
const relativeImports = [
|
|
169
138
|
...prefixLines.filter((line) => /^import .* from '\.\//u.test(line)),
|
|
170
139
|
importLine,
|
|
171
|
-
assetsImportLine,
|
|
172
140
|
].sort((left, right) => left.localeCompare(right));
|
|
173
141
|
const preservedPrefix = prefixLines.filter((line) => !/^import .* from '\.\//u.test(line));
|
|
174
142
|
const withImport = `${[...preservedPrefix, ...relativeImports].join('\n')}\n${source.slice(exportIndex + 1)}`;
|
|
@@ -182,7 +150,6 @@ function updateCategoryRegistry(source, definition) {
|
|
|
182
150
|
label: '${escapeSingleQuoted(definition.label)}',
|
|
183
151
|
description: '${escapeSingleQuoted(definition.description)}',
|
|
184
152
|
create: ${definition.factoryName},
|
|
185
|
-
assets: ${definition.assetsName},
|
|
186
153
|
},
|
|
187
154
|
`;
|
|
188
155
|
return `${withImport.slice(0, closeIndex)}${entry}${withImport.slice(closeIndex)}`;
|
package/package.json
CHANGED
|
@@ -1,361 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import { lstat, readFile } from 'node:fs/promises';
|
|
3
|
-
import { dirname, relative, resolve, sep } from 'node:path';
|
|
4
|
-
|
|
5
|
-
const IMAGE_CONTENT_TYPES = {
|
|
6
|
-
'image/avif': '.avif',
|
|
7
|
-
'image/gif': '.gif',
|
|
8
|
-
'image/jpeg': '.jpg',
|
|
9
|
-
'image/png': '.png',
|
|
10
|
-
'image/svg+xml': '.svg',
|
|
11
|
-
'image/webp': '.webp',
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
/*** Prepare immutable template evidence and runtime images through the released Templates asset API. */
|
|
15
|
-
export async function prepareTemplateImagery(input, targetDirectory, manifest, owners) {
|
|
16
|
-
const imagery = input.imagery ?? {};
|
|
17
|
-
assertRecord(imagery, 'imagery');
|
|
18
|
-
const root = `assets/templates/${input.category.replaceAll('_', '-')}/${input.templateId}`;
|
|
19
|
-
const conceptSeries = await prepareEvidenceSeries(
|
|
20
|
-
imagery.conceptSeries ?? [],
|
|
21
|
-
'concept series',
|
|
22
|
-
`${root}/evidence/concepts`,
|
|
23
|
-
);
|
|
24
|
-
const runtimeCaptures = await prepareEvidenceSeries(
|
|
25
|
-
imagery.runtimeCaptures ?? [],
|
|
26
|
-
'runtime captures',
|
|
27
|
-
`${root}/evidence/runtime-captures`,
|
|
28
|
-
);
|
|
29
|
-
const runtimeAssets = await prepareRuntimeAssets(
|
|
30
|
-
imagery.runtimeAssets ?? [],
|
|
31
|
-
`${root}/runtime`,
|
|
32
|
-
`assets/media/${input.templateId}`,
|
|
33
|
-
);
|
|
34
|
-
const resolvedManifest = registerRuntimeAssets(manifest, runtimeAssets);
|
|
35
|
-
assertRuntimeAssetsAreReferenced(resolvedManifest, runtimeAssets);
|
|
36
|
-
const composition = owners.templates.validateTemplateManifest(resolvedManifest, 'release');
|
|
37
|
-
if (composition.status !== 'ready') {
|
|
38
|
-
throw new Error(
|
|
39
|
-
`Manifest is not release-ready after imagery registration: ${composition.diagnostics.map((item) => item.message).join('; ')}`,
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
const readyManifest = owners.templates.assertTemplateManifestReady(composition);
|
|
43
|
-
const artifact = owners.templates.createStarterTemplateArtifact(
|
|
44
|
-
readyManifest,
|
|
45
|
-
runtimeAssets.map((asset) => asset.descriptor),
|
|
46
|
-
);
|
|
47
|
-
const files = [...conceptSeries, ...runtimeCaptures, ...runtimeAssets].sort((left, right) =>
|
|
48
|
-
left.destinationPath.localeCompare(right.destinationPath),
|
|
49
|
-
);
|
|
50
|
-
assertUniqueDestinations(files);
|
|
51
|
-
|
|
52
|
-
return {
|
|
53
|
-
artifact,
|
|
54
|
-
files,
|
|
55
|
-
inventory: {
|
|
56
|
-
conceptSeries: conceptSeries.map(toEvidenceInventory),
|
|
57
|
-
runtimeCaptures: runtimeCaptures.map(toEvidenceInventory),
|
|
58
|
-
runtimeAssets: runtimeAssets.map(toRuntimeAssetInventory),
|
|
59
|
-
},
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/*** Write only prevalidated copied image bytes into their canonical checked-in Templates paths. */
|
|
64
|
-
export async function writeTemplateImagery(files, targetDirectory, writeFile, mkdir) {
|
|
65
|
-
for (const file of files) {
|
|
66
|
-
const destination = resolve(targetDirectory, file.destinationPath);
|
|
67
|
-
assertInside(targetDirectory, destination, 'Template imagery destination');
|
|
68
|
-
await assertDestinationIsMissing(destination, file.destinationPath);
|
|
69
|
-
await mkdir(dirname(destination), { recursive: true });
|
|
70
|
-
await writeFile(destination, file.contents);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/*** Reject accidental replacement of an existing checked-in image outside the new variant boundary. */
|
|
75
|
-
async function assertDestinationIsMissing(destination, relativeDestination) {
|
|
76
|
-
try {
|
|
77
|
-
await lstat(destination);
|
|
78
|
-
} catch (error) {
|
|
79
|
-
if (isNotFoundError(error)) return;
|
|
80
|
-
throw error;
|
|
81
|
-
}
|
|
82
|
-
throw new Error(`Template imagery destination already exists: ${relativeDestination}`);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/*** Prepare one ordered concept or runtime-capture series without confusing evidence for runtime media. */
|
|
86
|
-
async function prepareEvidenceSeries(items, label, destinationDirectory) {
|
|
87
|
-
if (!Array.isArray(items)) throw new Error(`${label} must be an array.`);
|
|
88
|
-
const prepared = await Promise.all(
|
|
89
|
-
items.map(async (item) => {
|
|
90
|
-
assertRecord(item, `${label} item`);
|
|
91
|
-
const id = readNonEmptyString(item.id, `${label} item id`);
|
|
92
|
-
const order = readOrder(item.order, `${label} item ${id} order`);
|
|
93
|
-
const mode = readNonEmptyString(item.mode, `${label} item ${id} mode`);
|
|
94
|
-
const state = readNonEmptyString(item.state, `${label} item ${id} state`);
|
|
95
|
-
const image = await readSourceImage(item, `${label} item ${id}`);
|
|
96
|
-
return {
|
|
97
|
-
...image,
|
|
98
|
-
id,
|
|
99
|
-
order,
|
|
100
|
-
mode,
|
|
101
|
-
state,
|
|
102
|
-
destinationPath: `${destinationDirectory}/${String(order).padStart(3, '0')}-${safeStem(id)}${image.extension}`,
|
|
103
|
-
};
|
|
104
|
-
}),
|
|
105
|
-
);
|
|
106
|
-
assertUniqueIds(prepared, label);
|
|
107
|
-
assertUniqueOrders(prepared, label);
|
|
108
|
-
return prepared.sort(
|
|
109
|
-
(left, right) => left.order - right.order || left.id.localeCompare(right.id),
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/*** Prepare a runtime image, deriving both its packaged source and generated-project target paths. */
|
|
114
|
-
async function prepareRuntimeAssets(items, sourceDirectory, targetDirectory) {
|
|
115
|
-
if (!Array.isArray(items)) throw new Error('runtimeAssets must be an array.');
|
|
116
|
-
const prepared = await Promise.all(
|
|
117
|
-
items.map(async (item) => {
|
|
118
|
-
assertRecord(item, 'runtime asset');
|
|
119
|
-
const mediaId = readNonEmptyString(item.mediaId, 'runtime asset mediaId');
|
|
120
|
-
const name = readNonEmptyString(item.name, `runtime asset ${mediaId} name`);
|
|
121
|
-
const image = await readSourceImage(item, `runtime asset ${mediaId}`);
|
|
122
|
-
const stem = safeStem(mediaId);
|
|
123
|
-
const sourcePath = `${sourceDirectory}/${stem}${image.extension}`;
|
|
124
|
-
const targetPath = `${targetDirectory}/${stem}${image.extension}`;
|
|
125
|
-
return {
|
|
126
|
-
...image,
|
|
127
|
-
mediaId,
|
|
128
|
-
name,
|
|
129
|
-
destinationPath: sourcePath,
|
|
130
|
-
descriptor: {
|
|
131
|
-
sourcePath,
|
|
132
|
-
targetPath,
|
|
133
|
-
contentType: image.contentType,
|
|
134
|
-
sizeBytes: image.sizeBytes,
|
|
135
|
-
sha256: image.sha256,
|
|
136
|
-
},
|
|
137
|
-
};
|
|
138
|
-
}),
|
|
139
|
-
);
|
|
140
|
-
assertUniqueIds(prepared, 'runtime assets', 'mediaId');
|
|
141
|
-
return prepared.sort((left, right) => left.mediaId.localeCompare(right.mediaId));
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/*** Read immutable image bytes and the complete provenance required for a durable template record. */
|
|
145
|
-
async function readSourceImage(item, label) {
|
|
146
|
-
const sourcePath = readNonEmptyString(item.sourcePath, `${label} sourcePath`);
|
|
147
|
-
if (sourcePath.startsWith('blob:') || sourcePath.startsWith('data:')) {
|
|
148
|
-
throw new Error(`${label} sourcePath must name a durable image file, not a transient URL.`);
|
|
149
|
-
}
|
|
150
|
-
const absoluteSourcePath = resolve(sourcePath);
|
|
151
|
-
const stats = await lstat(absoluteSourcePath);
|
|
152
|
-
if (!stats.isFile() || stats.isSymbolicLink()) {
|
|
153
|
-
throw new Error(`${label} sourcePath must be a regular non-symlink file.`);
|
|
154
|
-
}
|
|
155
|
-
const contentType = readContentType(item.contentType, label);
|
|
156
|
-
const width = readPositiveInteger(item.width, `${label} width`);
|
|
157
|
-
const height = readPositiveInteger(item.height, `${label} height`);
|
|
158
|
-
const origin = readNonEmptyString(item.origin, `${label} origin`);
|
|
159
|
-
assertRecord(item.provenance, `${label} provenance`);
|
|
160
|
-
const contents = await readFile(absoluteSourcePath);
|
|
161
|
-
if (contents.byteLength === 0) throw new Error(`${label} sourcePath must not be empty.`);
|
|
162
|
-
return {
|
|
163
|
-
contents,
|
|
164
|
-
contentType,
|
|
165
|
-
extension: IMAGE_CONTENT_TYPES[contentType],
|
|
166
|
-
height,
|
|
167
|
-
origin,
|
|
168
|
-
provenance: item.provenance,
|
|
169
|
-
sha256: createHash('sha256').update(contents).digest('hex'),
|
|
170
|
-
sizeBytes: contents.byteLength,
|
|
171
|
-
width,
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/*** Register each checked-in runtime file once in the canonical manifest media pool. */
|
|
176
|
-
function registerRuntimeAssets(manifest, runtimeAssets) {
|
|
177
|
-
const resolvedManifest = structuredClone(manifest);
|
|
178
|
-
const media = resolvedManifest.media ?? { assets: {} };
|
|
179
|
-
const assets = media.assets ?? {};
|
|
180
|
-
for (const asset of runtimeAssets) {
|
|
181
|
-
if (assets[asset.mediaId] !== undefined) {
|
|
182
|
-
throw new Error(`Manifest media already defines runtime asset: ${asset.mediaId}`);
|
|
183
|
-
}
|
|
184
|
-
assets[asset.mediaId] = {
|
|
185
|
-
id: asset.mediaId,
|
|
186
|
-
name: asset.name,
|
|
187
|
-
kind: 'image',
|
|
188
|
-
source: { kind: 'bundled', path: asset.descriptor.targetPath },
|
|
189
|
-
contentType: asset.contentType,
|
|
190
|
-
metadata: {
|
|
191
|
-
sizeBytes: asset.sizeBytes,
|
|
192
|
-
width: asset.width,
|
|
193
|
-
height: asset.height,
|
|
194
|
-
},
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
resolvedManifest.media = { ...media, assets };
|
|
198
|
-
return resolvedManifest;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/*** Require every final runtime image asset to serve an exact canonical ZORA Image media reference. */
|
|
202
|
-
function assertRuntimeAssetsAreReferenced(manifest, runtimeAssets) {
|
|
203
|
-
const imageMediaIds = new Set();
|
|
204
|
-
for (const screen of Object.values(manifest.screens ?? {})) {
|
|
205
|
-
collectImageMediaIds(screen.root, imageMediaIds);
|
|
206
|
-
}
|
|
207
|
-
for (const runtimeAsset of runtimeAssets) {
|
|
208
|
-
if (!imageMediaIds.has(runtimeAsset.mediaId)) {
|
|
209
|
-
throw new Error(
|
|
210
|
-
`Runtime asset is not referenced by a ZORA Image mediaId: ${runtimeAsset.mediaId}`,
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/*** Collect only explicit Image source media references from one recursively composed manifest tree. */
|
|
217
|
-
function collectImageMediaIds(node, imageMediaIds) {
|
|
218
|
-
if (!isRecord(node)) return;
|
|
219
|
-
if (node.type === 'Image') {
|
|
220
|
-
const mediaId = node.props?.source?.mediaId;
|
|
221
|
-
if (typeof mediaId !== 'string' || mediaId === '') {
|
|
222
|
-
throw new Error('Every final ZORA Image must use one canonical source.mediaId reference.');
|
|
223
|
-
}
|
|
224
|
-
imageMediaIds.add(mediaId);
|
|
225
|
-
}
|
|
226
|
-
for (const child of Array.isArray(node.children) ? node.children : []) {
|
|
227
|
-
collectImageMediaIds(child, imageMediaIds);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/*** Project evidence records into a portable artifact inventory without temporary input paths. */
|
|
232
|
-
function toEvidenceInventory(item) {
|
|
233
|
-
return {
|
|
234
|
-
id: item.id,
|
|
235
|
-
path: item.destinationPath,
|
|
236
|
-
order: item.order,
|
|
237
|
-
mode: item.mode,
|
|
238
|
-
state: item.state,
|
|
239
|
-
contentType: item.contentType,
|
|
240
|
-
sizeBytes: item.sizeBytes,
|
|
241
|
-
width: item.width,
|
|
242
|
-
height: item.height,
|
|
243
|
-
origin: item.origin,
|
|
244
|
-
provenance: item.provenance,
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/*** Project a runtime image into artifact evidence and its canonical manifest/project references. */
|
|
249
|
-
function toRuntimeAssetInventory(item) {
|
|
250
|
-
return {
|
|
251
|
-
mediaId: item.mediaId,
|
|
252
|
-
sourcePath: item.descriptor.sourcePath,
|
|
253
|
-
targetPath: item.descriptor.targetPath,
|
|
254
|
-
contentType: item.contentType,
|
|
255
|
-
sizeBytes: item.sizeBytes,
|
|
256
|
-
width: item.width,
|
|
257
|
-
height: item.height,
|
|
258
|
-
origin: item.origin,
|
|
259
|
-
provenance: item.provenance,
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/*** Reject duplicate final destinations before any target repository mutation begins. */
|
|
264
|
-
function assertUniqueDestinations(files) {
|
|
265
|
-
const destinations = new Set();
|
|
266
|
-
for (const file of files) {
|
|
267
|
-
if (destinations.has(file.destinationPath)) {
|
|
268
|
-
throw new Error(`Template imagery destination is duplicated: ${file.destinationPath}`);
|
|
269
|
-
}
|
|
270
|
-
destinations.add(file.destinationPath);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/*** Reject duplicate source identities while preserving explicit property-specific diagnostics. */
|
|
275
|
-
function assertUniqueIds(items, label, field = 'id') {
|
|
276
|
-
const ids = new Set();
|
|
277
|
-
for (const item of items) {
|
|
278
|
-
const id = item[field];
|
|
279
|
-
if (ids.has(id)) throw new Error(`${label} has a duplicate ${field}: ${id}`);
|
|
280
|
-
ids.add(id);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/*** Require a meaningful sequence position for each durable evidence image. */
|
|
285
|
-
function assertUniqueOrders(items, label) {
|
|
286
|
-
const orders = new Set();
|
|
287
|
-
for (const item of items) {
|
|
288
|
-
if (orders.has(item.order)) throw new Error(`${label} has a duplicate order: ${item.order}`);
|
|
289
|
-
orders.add(item.order);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/*** Turn arbitrary durable identities into stable collision-resistant portable file stems. */
|
|
294
|
-
function safeStem(value) {
|
|
295
|
-
const normalized = value
|
|
296
|
-
.normalize('NFKD')
|
|
297
|
-
.replace(/[^a-zA-Z0-9]+/gu, '-')
|
|
298
|
-
.replace(/^-+|-+$/gu, '')
|
|
299
|
-
.toLowerCase();
|
|
300
|
-
const readable = normalized === '' ? 'image' : normalized;
|
|
301
|
-
return `${readable}-${createHash('sha256').update(value).digest('hex').slice(0, 12)}`;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/*** Require one supported image content type and rely on that owner-compatible extension mapping. */
|
|
305
|
-
function readContentType(value, label) {
|
|
306
|
-
if (typeof value !== 'string' || !(value in IMAGE_CONTENT_TYPES)) {
|
|
307
|
-
throw new Error(`${label} contentType must be one supported bundled image content type.`);
|
|
308
|
-
}
|
|
309
|
-
return value;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/*** Require an explicit non-empty string rather than inferring immutable artifact metadata. */
|
|
313
|
-
function readNonEmptyString(value, label) {
|
|
314
|
-
if (typeof value !== 'string' || value.trim() === '')
|
|
315
|
-
throw new Error(`${label} must be non-empty.`);
|
|
316
|
-
return value;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/*** Require real measured dimensions and an ordered series index. */
|
|
320
|
-
function readPositiveInteger(value, label) {
|
|
321
|
-
if (!Number.isSafeInteger(value) || value <= 0)
|
|
322
|
-
throw new Error(`${label} must be a positive integer.`);
|
|
323
|
-
return value;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/*** Read an explicit nonnegative evidence-series position. */
|
|
327
|
-
function readOrder(value, label) {
|
|
328
|
-
if (!Number.isSafeInteger(value) || value < 0)
|
|
329
|
-
throw new Error(`${label} must be a nonnegative integer.`);
|
|
330
|
-
return value;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/*** Reject source and output path escapes at the filesystem handoff boundary. */
|
|
334
|
-
function assertInside(parentDirectory, childPath, label) {
|
|
335
|
-
const relativePath = relative(parentDirectory, childPath);
|
|
336
|
-
if (
|
|
337
|
-
relativePath === '' ||
|
|
338
|
-
relativePath === '..' ||
|
|
339
|
-
relativePath.startsWith(`..${sep}`) ||
|
|
340
|
-
relativePath.includes(`${sep}..${sep}`)
|
|
341
|
-
) {
|
|
342
|
-
throw new Error(`${label} escapes its owner directory: ${childPath}`);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/*** Narrow an unknown input value to one non-array record. */
|
|
347
|
-
function assertRecord(value, label) {
|
|
348
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
349
|
-
throw new Error(`${label} must be an object.`);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/*** Narrow filesystem failures to the missing-path condition needed for no-overwrite protection. */
|
|
354
|
-
function isNotFoundError(error) {
|
|
355
|
-
return error instanceof Error && 'code' in error && error.code === 'ENOENT';
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/*** Narrow unknown manifest data to object-shaped node records. */
|
|
359
|
-
function isRecord(value) {
|
|
360
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
361
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/*** Remove every temporary Templates fixture created by imagery behavior tests. */
|
|
2
|
-
export declare function cleanupTemplatesFixtures(): Promise<void>;
|
|
3
|
-
/*** Set up the minimum released owner surface required by the packaged skill script. */
|
|
4
|
-
export declare function createTemplatesFixture(): Promise<string>;
|
|
5
|
-
/*** Create one complete input whose manifest is ready before imagery registration. */
|
|
6
|
-
export declare function createScaffoldInput(targetDirectory: string, templateId: string): Record<string, unknown>;
|
|
7
|
-
/*** Write deterministic source images that the skill must retain inside the target template. */
|
|
8
|
-
export declare function writeFixtureImages(target: string): Promise<Record<'capture' | 'concept' | 'hero', string>>;
|
|
9
|
-
/*** Create one complete ordered evidence record for a durable fixture image. */
|
|
10
|
-
export declare function imageEvidence(id: string, sourcePath: string, contentType: string, width: number, height: number, mode: string, state: string, order: number): Record<string, unknown>;
|
|
11
|
-
/*** Write deterministic JSON input to the temporary target repository. */
|
|
12
|
-
export declare function writeJson(path: string, value: unknown): Promise<void>;
|
|
13
|
-
/*** Return the one category registry path owned by this focused fixture. */
|
|
14
|
-
export declare function categoryPath(target: string): string;
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
const temporaryDirectories = [];
|
|
4
|
-
/*** Remove every temporary Templates fixture created by imagery behavior tests. */
|
|
5
|
-
export async function cleanupTemplatesFixtures() {
|
|
6
|
-
await Promise.all(temporaryDirectories
|
|
7
|
-
.splice(0)
|
|
8
|
-
.map((directory) => rm(directory, { force: true, recursive: true })));
|
|
9
|
-
}
|
|
10
|
-
/*** Set up the minimum released owner surface required by the packaged skill script. */
|
|
11
|
-
export async function createTemplatesFixture() {
|
|
12
|
-
const target = await mkdtemp('/tmp/devtools-zora-imagery-');
|
|
13
|
-
temporaryDirectories.push(target);
|
|
14
|
-
await writeJson(join(target, 'package.json'), {
|
|
15
|
-
name: '@ankhorage/templates',
|
|
16
|
-
version: '9.0.0',
|
|
17
|
-
type: 'module',
|
|
18
|
-
exports: { '.': './index.js', './package.json': './package.json' },
|
|
19
|
-
});
|
|
20
|
-
await writeFile(join(target, 'index.js'), TEMPLATES_FIXTURE_SOURCE);
|
|
21
|
-
await writeFixturePackage(target, '@ankhorage/zora', '4.0.0', {
|
|
22
|
-
'./theme': './theme.js',
|
|
23
|
-
'./metadata': './metadata.js',
|
|
24
|
-
'./package.json': './package.json',
|
|
25
|
-
});
|
|
26
|
-
const zoraDirectory = join(target, 'node_modules/@ankhorage/zora');
|
|
27
|
-
await writeFile(join(zoraDirectory, 'theme.js'), 'export const compileZoraTheme = () => ({});\n');
|
|
28
|
-
await writeFile(join(zoraDirectory, 'metadata.js'), 'export const ZORA_COMPONENT_META = {}; export const ZORA_THEME_RECIPE_META = {};\n');
|
|
29
|
-
await mkdir(categoryPath(target), { recursive: true });
|
|
30
|
-
await writeFile(join(categoryPath(target), 'index.ts'), CATEGORY_REGISTRY_SOURCE);
|
|
31
|
-
return target;
|
|
32
|
-
}
|
|
33
|
-
/*** Create one complete input whose manifest is ready before imagery registration. */
|
|
34
|
-
export function createScaffoldInput(targetDirectory, templateId) {
|
|
35
|
-
return {
|
|
36
|
-
targetDirectory,
|
|
37
|
-
category: 'business_productivity',
|
|
38
|
-
templateId,
|
|
39
|
-
label: 'Evidence Board',
|
|
40
|
-
description: 'A metadata-backed evidence review starter.',
|
|
41
|
-
artifact: { status: 'resolved', source: { mode: 'template', inputs: [], evidence: [] } },
|
|
42
|
-
manifest: createManifest(),
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
/*** Write deterministic source images that the skill must retain inside the target template. */
|
|
46
|
-
export async function writeFixtureImages(target) {
|
|
47
|
-
const concept = join(target, 'concept.png');
|
|
48
|
-
const capture = join(target, 'capture.png');
|
|
49
|
-
const hero = join(target, 'hero.svg');
|
|
50
|
-
await Promise.all([
|
|
51
|
-
writeFile(concept, 'concept-image'),
|
|
52
|
-
writeFile(capture, 'runtime-capture'),
|
|
53
|
-
writeFile(hero, '<svg xmlns="http://www.w3.org/2000/svg"/>'),
|
|
54
|
-
]);
|
|
55
|
-
return { concept, capture, hero };
|
|
56
|
-
}
|
|
57
|
-
/*** Create one complete ordered evidence record for a durable fixture image. */
|
|
58
|
-
export function imageEvidence(id, sourcePath, contentType, width, height, mode, state, order) {
|
|
59
|
-
return {
|
|
60
|
-
id,
|
|
61
|
-
sourcePath,
|
|
62
|
-
contentType,
|
|
63
|
-
width,
|
|
64
|
-
height,
|
|
65
|
-
mode,
|
|
66
|
-
state,
|
|
67
|
-
order,
|
|
68
|
-
origin: 'generated',
|
|
69
|
-
provenance: { tool: 'fixture' },
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
/*** Write deterministic JSON input to the temporary target repository. */
|
|
73
|
-
export async function writeJson(path, value) {
|
|
74
|
-
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
75
|
-
}
|
|
76
|
-
/*** Return the one category registry path owned by this focused fixture. */
|
|
77
|
-
export function categoryPath(target) {
|
|
78
|
-
return join(target, 'src/templates/starter/categories/business-productivity');
|
|
79
|
-
}
|
|
80
|
-
/*** Install one minimal public ESM package fixture under the target repository. */
|
|
81
|
-
async function writeFixturePackage(target, packageName, version, exports) {
|
|
82
|
-
const directory = join(target, 'node_modules', packageName);
|
|
83
|
-
await mkdir(directory, { recursive: true });
|
|
84
|
-
await writeJson(join(directory, 'package.json'), {
|
|
85
|
-
name: packageName,
|
|
86
|
-
version,
|
|
87
|
-
type: 'module',
|
|
88
|
-
exports,
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
/*** Create the released-ready base manifest used by the owner fixture. */
|
|
92
|
-
function createManifest() {
|
|
93
|
-
const theme = {
|
|
94
|
-
id: 'evidence-theme',
|
|
95
|
-
name: 'Evidence Theme',
|
|
96
|
-
light: { primaryColor: '#2563EB', harmony: 'complementary' },
|
|
97
|
-
dark: { primaryColor: '#2563EB', harmony: 'complementary' },
|
|
98
|
-
};
|
|
99
|
-
return {
|
|
100
|
-
metadata: {
|
|
101
|
-
name: 'Evidence Board',
|
|
102
|
-
slug: 'evidence-board',
|
|
103
|
-
version: '1.0.0',
|
|
104
|
-
category: 'business_productivity',
|
|
105
|
-
themeId: theme.id,
|
|
106
|
-
},
|
|
107
|
-
themes: [theme],
|
|
108
|
-
activeThemeId: theme.id,
|
|
109
|
-
infra: { modules: [] },
|
|
110
|
-
navigator: { type: 'stack', routes: [{ name: 'index', screenId: 'home' }] },
|
|
111
|
-
screens: {
|
|
112
|
-
home: {
|
|
113
|
-
id: 'home',
|
|
114
|
-
name: 'Home',
|
|
115
|
-
root: {
|
|
116
|
-
id: 'home-root',
|
|
117
|
-
type: 'View',
|
|
118
|
-
children: [
|
|
119
|
-
{ id: 'hero-image', type: 'Image', props: { source: { mediaId: 'hero-image' } } },
|
|
120
|
-
],
|
|
121
|
-
},
|
|
122
|
-
},
|
|
123
|
-
},
|
|
124
|
-
settings: { localization: { defaultLocale: 'en', locales: ['en'] } },
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
const CATEGORY_REGISTRY_SOURCE = `import type { CategoryStarterTemplateDefinition } from '../../starter.types';
|
|
128
|
-
|
|
129
|
-
export const businessProductivityStarterTemplates = [] satisfies readonly CategoryStarterTemplateDefinition[];
|
|
130
|
-
`;
|
|
131
|
-
const TEMPLATES_FIXTURE_SOURCE = `
|
|
132
|
-
export const CATEGORY_PRESETS = {};
|
|
133
|
-
export const TONE_PAIR_CATALOG = [];
|
|
134
|
-
export const resolveTonePair = () => null;
|
|
135
|
-
export const resolveCategoryDesignPreset = () => ({});
|
|
136
|
-
export const compileCategoryDesign = () => ({});
|
|
137
|
-
export const composeCategoryAppManifest = () => ({});
|
|
138
|
-
export const validateTemplateManifest = (manifest) => ({ manifest, diagnostics: [], status: 'ready' });
|
|
139
|
-
export const assertTemplateManifestReady = (composition) => composition.manifest;
|
|
140
|
-
export const createStarterTemplateArtifact = (manifest, assets) => ({ manifest, assets });
|
|
141
|
-
`;
|