@ankhorage/devtools 1.10.3 → 1.10.5

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.
@@ -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. Final runtime
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. Before final template release, retain the original
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, target
123
- root, artifact input, and optional `imagery` inventory. The helper verifies the Templates repository
124
- and owner validation, creates a normal variant `manifest.ts`, `template.ts`, `assets.ts`, and
125
- `index.ts`, updates the category registry import and definition deterministically, and writes
126
- `zora-designer.md` with durable visual-asset inventory.
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: '9.0.0' },
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
- ...Object.keys(files).map((fileName) =>
98
- relative(targetDirectory, join(variantDirectory, fileName)),
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, assets, assetsName }) {
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
- const assetsImportLine = `import { ${definition.assetsName} } from './${definition.templateId}/assets';`;
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@ankhorage/devtools",
3
- "version": "1.10.3",
3
+ "version": "1.10.5",
4
4
  "description": "Shared development tools and repository standards for Ankhorage",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/ankhorage/devtools#readme",
@@ -124,7 +124,7 @@
124
124
  "typescript-eslint": "^8.69.0"
125
125
  },
126
126
  "devDependencies": {
127
- "@ankhorage/ankh": "^0.8.8",
127
+ "@ankhorage/ankh": "^0.8.9",
128
128
  "@ankhorage/doctor": "0.10.6",
129
129
  "@types/bun": "^1.4.0",
130
130
  "@types/node": "^26.4.1",
@@ -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
- `;