@ankhorage/devtools 1.13.0 → 1.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,16 +1,16 @@
1
1
  ---
2
2
  name: zora-designer
3
3
  description: >
4
- Configure an owner-backed application design, generate one screen or a coherent screen series,
5
- audit supplied evidence, and author portable Ankhorage templates. Use for category-driven design
6
- decisions, ZORA screen generation, visual audits, or template creation.
4
+ Configure an owner-backed application design, generate or recognize screens, audit supplied
5
+ evidence, and author portable Ankhorage templates. Use for category-driven design decisions,
6
+ ZORA screen generation or reconstruction, visual audits, or template creation.
7
7
  ---
8
8
 
9
9
  # ZORA Designer
10
10
 
11
- Design, audit, and author through the target repository's released owner APIs. The complete
12
- `AppManifest` is runtime authority; `zora-designer.md` and generated screen images are design
13
- evidence.
11
+ Design, recognize, audit, and author through the target repository's released owner APIs. The
12
+ complete `AppManifest` is runtime authority; `zora-designer.md` and generated screen images are
13
+ design evidence.
14
14
 
15
15
  ## Route the request
16
16
 
@@ -20,6 +20,10 @@ evidence.
20
20
  screen.
21
21
  - `screens`: resolve the configuration, then read [screens.md](references/screens.md) and design an
22
22
  ordered series with shared navigation, state, geometry, and tokens.
23
+ - `recognize`: when the user supplies an existing UI screenshot/screen design and asks to
24
+ reconstruct it, determine ZORA components, or convert it to a manifest screen, read
25
+ [screen-analysis.md](references/screen-analysis.md) and run programmatic recognition before
26
+ free-form visual interpretation.
23
27
  - `audit`: read [audit.md](references/audit.md) and evaluate supplied image or runtime evidence.
24
28
  - `template`: resolve the configuration and screen model, then author one portable template through
25
29
  the workflow below.
@@ -45,6 +49,10 @@ skill.
45
49
  The inspection composes metadata-only descriptors from every installed `@ankhorage/zora-*` plugin;
46
50
  use those plugin elements exactly like ZORA core elements and keep their package provenance.
47
51
 
52
+ For supplied-screen reconstruction, continue through `recognize` and treat the local analyzer's
53
+ `ScreenSpec`, visual graph, confidence, alternatives, and unresolved diagnostics as the structural
54
+ evidence baseline. Do not ask an image model to redo deterministic geometry/component matching.
55
+
48
56
  Compile chosen values with the same helper before composing screens. Inspect both computed modes,
49
57
  including their resolved Surface themes and all owner diagnostics. Never hand-calculate a value the
50
58
  owner exposes.
@@ -52,9 +60,15 @@ owner exposes.
52
60
  ## Preserve the complete UX
53
61
 
54
62
  For every screen region, prefer the exact semantic ZORA element supported by current metadata.
55
- Visual resemblance alone is insufficient. If no exact element exists, preserve the requested UX
56
- with an obvious supported placeholder such as a secondary-surface `Box`, and record the capability
57
- gap. Do not invent props, application components, or successful behavior.
63
+ Visual resemblance alone is insufficient. If programmatic recognition leaves multiple plausible
64
+ candidates, resolve them from semantic responsibility and stated product intent while preserving the
65
+ ranked alternatives as evidence.
66
+
67
+ If no exact element exists, preserve the requested UX with an explicit owner-supported unresolved
68
+ placeholder when available, and record the capability gap. For ordinary design composition where no
69
+ unresolved manifest element is applicable, use an obvious supported placeholder such as a
70
+ secondary-surface `Box` and record the capability gap. Do not invent props, application components,
71
+ or successful behavior.
58
72
 
59
73
  Bind every interaction expressible by installed Contracts and ZORA event metadata. Leave an
60
74
  unsupported interaction visibly present and explicitly unbound without blocking unrelated design
@@ -98,6 +112,8 @@ barrels.
98
112
 
99
113
  - confirm the interactive decision sequence completed or the user explicitly accepted remaining
100
114
  recommendations;
115
+ - for supplied image reconstruction, run `recognize` before semantic refinement and retain its
116
+ confidence/alternative/gap evidence;
101
117
  - compile light and dark independently through installed owner APIs;
102
118
  - validate selected ZORA nodes, props, events, actions, and complete manifest contracts;
103
119
  - run the screen composition gate from [screens.md](references/screens.md) before returning screen
@@ -1,6 +1,6 @@
1
1
  interface:
2
2
  display_name: 'ZORA Designer'
3
- short_description: 'Design, audit, and author Ankhorage screens and templates through ZORA.'
4
- default_prompt: 'Use $zora-designer to design, audit, or author this Ankhorage interface.'
3
+ short_description: 'Design, recognize, audit, and author Ankhorage screens and templates through ZORA.'
4
+ default_prompt: 'Use $zora-designer to design, recognize, audit, or author this Ankhorage interface.'
5
5
  policy:
6
6
  allow_implicit_invocation: true
@@ -0,0 +1,115 @@
1
+ # Supplied Screen Recognition
2
+
3
+ Read this reference when the user supplies an existing UI screenshot or screen design and asks to
4
+ reconstruct it, determine its ZORA components, convert it to a manifest screen, or use it as the
5
+ starting point for a redesigned screen.
6
+
7
+ This path analyzes supplied evidence. It does not replace `screen` or `screens` generation.
8
+
9
+ ## Run deterministic recognition first
10
+
11
+ Before free-form visual interpretation, inspect the released owners and run the local analyzer from
12
+ the target repository:
13
+
14
+ ```text
15
+ bun .agents/skills/zora-designer/scripts/owner-api.ts inspect
16
+ bun .agents/skills/zora-designer/scripts/analyze-screen.ts screen-analysis-input.json
17
+ ```
18
+
19
+ The analyzer uses the current composed ZORA metadata, including every installed
20
+ `@ankhorage/zora-*` plugin, and delegates image processing to released
21
+ `@ankhorage/utility/image`:
22
+
23
+ ```text
24
+ supplied image
25
+ -> Sharp normalization
26
+ -> OpenCV geometry and visual graph
27
+ -> optional local Tesseract OCR
28
+ -> owner-metadata component matching
29
+ -> global tree validation
30
+ -> Contracts ScreenSpec
31
+ ```
32
+
33
+ No remote model or API is required for this pass.
34
+
35
+ ## Input
36
+
37
+ Write one temporary JSON input file in the target repository. Image paths are resolved from the
38
+ target repository root.
39
+
40
+ ```json
41
+ {
42
+ "image": "design/screens/home.png",
43
+ "screen": {
44
+ "id": "home",
45
+ "name": "Home",
46
+ "title": "Home"
47
+ },
48
+ "minConfidence": 0.42
49
+ }
50
+ ```
51
+
52
+ OCR is supplementary. Enable it only when local trained data is available and text evidence helps
53
+ distinguish visually similar components:
54
+
55
+ ```json
56
+ {
57
+ "image": "design/screens/home.png",
58
+ "screen": { "id": "home", "name": "Home" },
59
+ "ocr": {
60
+ "langPath": "./tessdata",
61
+ "language": "eng"
62
+ }
63
+ }
64
+ ```
65
+
66
+ Geometry-only analysis remains valid if OCR setup or recognition fails; the output records an OCR
67
+ diagnostic instead of replacing the visual graph.
68
+
69
+ ## Local engine boundary
70
+
71
+ `@ankhorage/utility` owns the image implementation. The target authoring repository supplies the
72
+ optional native/WASM engines required by the recognition path. If they are absent, install them as
73
+ development tooling:
74
+
75
+ ```text
76
+ bun add -D sharp @techstark/opencv-js
77
+ ```
78
+
79
+ Do not add a copied matcher, OpenCV wrapper, fallback parser, remote vision service, or alternate
80
+ component catalogue to the skill.
81
+
82
+ ## Treat the result as evidence
83
+
84
+ The analyzer returns:
85
+
86
+ - canonical Contracts `ScreenSpec`;
87
+ - visual graph evidence;
88
+ - aggregate confidence;
89
+ - ranked component candidates by region;
90
+ - unresolved diagnostics;
91
+ - the exact composed component names and owner/plugin version provenance used for the pass.
92
+
93
+ Use the generated `ScreenSpec` as the structural baseline. Do not discard a high-confidence exact
94
+ metadata match merely because another component looks visually similar.
95
+
96
+ When confidence is low, preserve the analyzer's alternatives and resolve the ambiguity using the
97
+ screen's semantic responsibility and the user's stated product intent. If metadata declares an
98
+ unresolved element, the analyzer uses that explicit owner-owned gap instead of inventing a ZORA
99
+ component or prop.
100
+
101
+ ## Refine only what pixels cannot prove
102
+
103
+ After recognition, reason about the parts a static image cannot establish reliably:
104
+
105
+ - purpose and primary task;
106
+ - actions and event bindings;
107
+ - navigation and back/cancel behavior;
108
+ - state transitions and validation;
109
+ - data sources and bindings;
110
+ - accessibility and focus behavior;
111
+ - responsive behavior beyond the observed viewport;
112
+ - loading, empty, error, offline, pressed, selected, and other unseen states.
113
+
114
+ Then run the normal `screen` composition rules and owner-backed manifest validation. A screenshot is
115
+ visual evidence, not proof of runtime behavior.
@@ -4,6 +4,11 @@ Read this reference for `screen` and `screens` after the interactive configurati
4
4
  Generated images are concept evidence; the manifest and ZORA metadata remain implementation
5
5
  authority.
6
6
 
7
+ When the starting point is a supplied existing screen image, run `recognize` first through
8
+ [screen-analysis.md](screen-analysis.md). Treat its canonical `ScreenSpec`, visual graph, confidence,
9
+ alternatives, and unresolved diagnostics as structural evidence, then use this reference for the
10
+ semantic and behavioral refinement that pixels cannot establish.
11
+
7
12
  ## Preconditions
8
13
 
9
14
  Do not generate or implement a screen until these are resolved:
@@ -30,6 +35,10 @@ For every screen record:
30
35
  6. safe areas, keyboard/overlay behavior, scroll ownership, and narrow/wide behavior;
31
36
  7. every SVG icon and real image region, its reusable asset ID, and its intended manifest usage.
32
37
 
38
+ For a recognized supplied image, preserve every high-confidence metadata-backed subtree unless the
39
+ user's stated intent provides stronger semantic evidence. Resolve ambiguous candidates explicitly;
40
+ do not silently replace the analyzer's tree with a visually similar primitive decomposition.
41
+
33
42
  Read [runtime-assets.md](runtime-assets.md). Produce and inspect the separate icons and images before
34
43
  concept rendering, use them as screen references, and deliver their checked bundle with the screens.
35
44
 
@@ -0,0 +1,320 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { readFile } from 'node:fs/promises';
4
+ import { createRequire } from 'node:module';
5
+ import { join, resolve } from 'node:path';
6
+ import { pathToFileURL } from 'node:url';
7
+
8
+ import { loadOwnerApis } from './owner-api.ts';
9
+
10
+ const DEVTOOLS_PACKAGE_NAME = '@ankhorage/devtools';
11
+ const UTILITY_PACKAGE_NAME = '@ankhorage/utility';
12
+ const REQUIRED_IMAGE_ENGINES = ['sharp', '@techstark/opencv-js'] as const;
13
+
14
+ interface ScreenOcr {
15
+ readonly recognizeAsync: (image: Uint8Array) => Promise<readonly unknown[]>;
16
+ readonly terminateAsync?: () => Promise<void>;
17
+ }
18
+
19
+ interface UtilityImageApi {
20
+ readonly analyzeScreenImageAsync: (
21
+ image: string,
22
+ options: Readonly<Record<string, unknown>>,
23
+ ) => Promise<unknown>;
24
+ readonly createTesseractScreenOcrAsync: (options: {
25
+ readonly langPath: string;
26
+ readonly language?: string;
27
+ }) => Promise<ScreenOcr>;
28
+ }
29
+
30
+ interface AnalyzeScreenInput {
31
+ readonly image: string;
32
+ readonly screen: {
33
+ readonly id: string;
34
+ readonly name: string;
35
+ readonly title?: string;
36
+ readonly description?: string;
37
+ };
38
+ readonly minConfidence?: number;
39
+ readonly ocr?: {
40
+ readonly langPath: string;
41
+ readonly language?: string;
42
+ };
43
+ }
44
+
45
+ interface ScreenAnalysisRuntime {
46
+ readonly api: UtilityImageApi;
47
+ readonly utilityVersion: string;
48
+ }
49
+
50
+ /*** Analyze one supplied UI screen with current owner metadata and the released Utility image pipeline. */
51
+ export async function analyzeScreenAsync(
52
+ input: unknown,
53
+ targetDirectory = process.cwd(),
54
+ ): Promise<Record<string, unknown>> {
55
+ const parsedInput = readAnalyzeScreenInput(input);
56
+ const owners = await loadOwnerApis(targetDirectory);
57
+ const components = readComponentCatalog(owners.zoraMetadata.ZORA_COMPONENT_META);
58
+ const unresolvedComponentName = resolveUnresolvedComponentName(components);
59
+ const runtime = await loadScreenAnalysisRuntime(targetDirectory);
60
+ const ocr = await createOptionalOcr(runtime.api, parsedInput.ocr);
61
+
62
+ try {
63
+ const analysis = await runtime.api.analyzeScreenImageAsync(
64
+ resolve(targetDirectory, parsedInput.image),
65
+ {
66
+ screen: parsedInput.screen,
67
+ components,
68
+ ...(unresolvedComponentName ? { unresolvedComponentName } : {}),
69
+ ...(parsedInput.minConfidence === undefined
70
+ ? {}
71
+ : { minConfidence: parsedInput.minConfidence }),
72
+ ...(ocr ? { ocr } : {}),
73
+ },
74
+ );
75
+ assertRecord(analysis, 'Utility screen analysis result');
76
+ return {
77
+ owners: { ...owners.versions, utility: runtime.utilityVersion },
78
+ componentCount: components.length,
79
+ componentNames: components.map((component) => String(component.name)).sort(),
80
+ ...(unresolvedComponentName ? { unresolvedComponentName } : {}),
81
+ ...analysis,
82
+ };
83
+ } finally {
84
+ await ocr?.terminateAsync?.();
85
+ }
86
+ }
87
+
88
+ /*** Load Utility image analysis through the installed Devtools dependency boundary. */
89
+ async function loadScreenAnalysisRuntime(targetDirectory: string): Promise<ScreenAnalysisRuntime> {
90
+ const devtoolsManifestPath = await resolveDevtoolsManifestPath(targetDirectory);
91
+ const devtoolsRequire = createRequire(devtoolsManifestPath);
92
+ let utilityManifestPath: string;
93
+ try {
94
+ utilityManifestPath = devtoolsRequire.resolve(`${UTILITY_PACKAGE_NAME}/package.json`);
95
+ } catch (error) {
96
+ throw new Error(
97
+ 'zora-designer screen analysis requires @ankhorage/utility 0.5.x through the installed @ankhorage/devtools dependency. Update Devtools through the normal release/Renovate workflow and retry.',
98
+ { cause: error },
99
+ );
100
+ }
101
+
102
+ assertRequiredImageEngines(devtoolsRequire);
103
+ const utilityManifest: unknown = JSON.parse(await readFile(utilityManifestPath, 'utf8'));
104
+ assertRecord(utilityManifest, '@ankhorage/utility package manifest');
105
+ assertNonEmptyString(utilityManifest.version, '@ankhorage/utility version');
106
+
107
+ let modulePath: string;
108
+ try {
109
+ modulePath = devtoolsRequire.resolve(`${UTILITY_PACKAGE_NAME}/image`);
110
+ } catch (error) {
111
+ throw new Error(
112
+ 'Installed @ankhorage/utility does not expose the required public /image subpath. Update @ankhorage/devtools so it selects Utility 0.5.x or newer and retry.',
113
+ { cause: error },
114
+ );
115
+ }
116
+ const module: unknown = await import(pathToFileURL(modulePath).href);
117
+ assertUtilityImageApi(module);
118
+ return { api: module, utilityVersion: utilityManifest.version };
119
+ }
120
+
121
+ /*** Resolve the installed Devtools package manifest used as the dependency-resolution anchor. */
122
+ async function resolveDevtoolsManifestPath(targetDirectory: string): Promise<string> {
123
+ const targetRoot = resolve(targetDirectory);
124
+ const targetManifestPath = join(targetRoot, 'package.json');
125
+ const targetManifest: unknown = JSON.parse(await readFile(targetManifestPath, 'utf8'));
126
+ assertRecord(targetManifest, 'Target package manifest');
127
+ if (targetManifest.name === DEVTOOLS_PACKAGE_NAME) {
128
+ return targetManifestPath;
129
+ }
130
+
131
+ const installedManifestPath = join(
132
+ targetRoot,
133
+ 'node_modules',
134
+ '@ankhorage',
135
+ 'devtools',
136
+ 'package.json',
137
+ );
138
+ try {
139
+ await readFile(installedManifestPath, 'utf8');
140
+ return installedManifestPath;
141
+ } catch (error) {
142
+ throw new Error(
143
+ 'zora-designer screen analysis requires the target repository to have @ankhorage/devtools installed. Run the normal Devtools synchronization/install workflow and retry.',
144
+ { cause: error },
145
+ );
146
+ }
147
+ }
148
+
149
+ /*** Require the local deterministic engines used by Utility without making them global Devtools runtime dependencies. */
150
+ function assertRequiredImageEngines(devtoolsRequire: ReturnType<typeof createRequire>): void {
151
+ const missing = REQUIRED_IMAGE_ENGINES.filter((packageName) => {
152
+ try {
153
+ devtoolsRequire.resolve(packageName);
154
+ return false;
155
+ } catch {
156
+ return true;
157
+ }
158
+ });
159
+ if (missing.length > 0) {
160
+ throw new Error(
161
+ `zora-designer screen analysis requires local image engines ${missing.join(', ')}. Install the screen-analysis engines in the target repository with "bun add -D sharp @techstark/opencv-js" and retry.`,
162
+ );
163
+ }
164
+ }
165
+
166
+ /*** Create supplementary local OCR while converting setup failures into evidence rather than geometry blockers. */
167
+ async function createOptionalOcr(
168
+ api: UtilityImageApi,
169
+ options: AnalyzeScreenInput['ocr'],
170
+ ): Promise<ScreenOcr | undefined> {
171
+ if (!options) return undefined;
172
+ try {
173
+ return await api.createTesseractScreenOcrAsync(options);
174
+ } catch (error) {
175
+ const rejection = error instanceof Error ? error : new Error(String(error));
176
+ return { recognizeAsync: () => Promise.reject(rejection) };
177
+ }
178
+ }
179
+
180
+ /*** Preserve the full composed owner metadata while validating the fields required by the Utility matcher. */
181
+ function readComponentCatalog(value: unknown): Record<string, unknown>[] {
182
+ assertRecord(value, 'Composed ZORA component metadata');
183
+ return Object.values(value)
184
+ .filter((component) => component !== undefined)
185
+ .map((component, index) => {
186
+ assertRecord(component, `ZORA component metadata[${index}]`);
187
+ assertNonEmptyString(component.name, `ZORA component metadata[${index}].name`);
188
+ assertComponentCategory(component.category, `ZORA component metadata[${index}].category`);
189
+ if (typeof component.directManifestNode !== 'boolean') {
190
+ throw new Error(`ZORA component metadata[${index}].directManifestNode must be a boolean.`);
191
+ }
192
+ assertStringArray(
193
+ component.allowedChildren,
194
+ `ZORA component metadata[${index}].allowedChildren`,
195
+ );
196
+ assertRecord(component.props, `ZORA component metadata[${index}].props`);
197
+ return component;
198
+ });
199
+ }
200
+
201
+ /*** Resolve the owner-declared unresolved manifest component without hard-coding a ZORA component name. */
202
+ function resolveUnresolvedComponentName(
203
+ components: readonly Record<string, unknown>[],
204
+ ): string | undefined {
205
+ const unresolved = components.find((component) => {
206
+ const policy = component.manifestPolicy;
207
+ return isRecord(policy) && policy.kind === 'unresolved-element';
208
+ });
209
+ return typeof unresolved?.name === 'string' ? unresolved.name : undefined;
210
+ }
211
+
212
+ /*** Parse and validate portable JSON input for one recognition operation. */
213
+ function readAnalyzeScreenInput(value: unknown): AnalyzeScreenInput {
214
+ assertRecord(value, 'Screen analysis input');
215
+ assertNonEmptyString(value.image, 'image');
216
+ assertRecord(value.screen, 'screen');
217
+ assertNonEmptyString(value.screen.id, 'screen.id');
218
+ assertNonEmptyString(value.screen.name, 'screen.name');
219
+ assertOptionalString(value.screen.title, 'screen.title');
220
+ assertOptionalString(value.screen.description, 'screen.description');
221
+ const minConfidence = readMinConfidence(value.minConfidence);
222
+ const ocr = readOcrOptions(value.ocr);
223
+
224
+ return {
225
+ image: value.image,
226
+ screen: {
227
+ id: value.screen.id,
228
+ name: value.screen.name,
229
+ ...(value.screen.title === undefined ? {} : { title: value.screen.title }),
230
+ ...(value.screen.description === undefined ? {} : { description: value.screen.description }),
231
+ },
232
+ ...(minConfidence === undefined ? {} : { minConfidence }),
233
+ ...(ocr === undefined ? {} : { ocr }),
234
+ };
235
+ }
236
+
237
+ /*** Read one optional confidence threshold constrained to the Utility matcher interval. */
238
+ function readMinConfidence(value: unknown): number | undefined {
239
+ if (value === undefined) return undefined;
240
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > 1) {
241
+ throw new Error('minConfidence must be a finite number from 0 through 1.');
242
+ }
243
+ return value;
244
+ }
245
+
246
+ /*** Read optional local OCR configuration without making OCR a geometry prerequisite. */
247
+ function readOcrOptions(value: unknown): AnalyzeScreenInput['ocr'] {
248
+ if (value === undefined) return undefined;
249
+ assertRecord(value, 'ocr');
250
+ assertNonEmptyString(value.langPath, 'ocr.langPath');
251
+ assertOptionalString(value.language, 'ocr.language');
252
+ return {
253
+ langPath: value.langPath,
254
+ ...(value.language === undefined ? {} : { language: value.language }),
255
+ };
256
+ }
257
+
258
+ /*** Narrow the released Utility image subpath to the two capabilities used by the skill. */
259
+ function assertUtilityImageApi(value: unknown): asserts value is UtilityImageApi {
260
+ assertRecord(value, '@ankhorage/utility/image');
261
+ if (typeof value.analyzeScreenImageAsync !== 'function') {
262
+ throw new Error('@ankhorage/utility/image must export analyzeScreenImageAsync.');
263
+ }
264
+ if (typeof value.createTesseractScreenOcrAsync !== 'function') {
265
+ throw new Error('@ankhorage/utility/image must export createTesseractScreenOcrAsync.');
266
+ }
267
+ }
268
+
269
+ /*** Require one current ZORA component category. */
270
+ function assertComponentCategory(value: unknown, label: string): void {
271
+ if (!['foundation', 'component', 'pattern', 'layout'].includes(String(value))) {
272
+ throw new Error(`${label} must be a current ZORA component category.`);
273
+ }
274
+ }
275
+
276
+ /*** Require an array containing only strings. */
277
+ function assertStringArray(value: unknown, label: string): asserts value is string[] {
278
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string')) {
279
+ throw new Error(`${label} must be a string array.`);
280
+ }
281
+ }
282
+
283
+ /*** Require an optional non-empty string when present. */
284
+ function assertOptionalString(value: unknown, label: string): asserts value is string | undefined {
285
+ if (value !== undefined) assertNonEmptyString(value, label);
286
+ }
287
+
288
+ /*** Require an object-shaped value. */
289
+ function assertRecord(value: unknown, label: string): asserts value is Record<string, unknown> {
290
+ if (!isRecord(value)) throw new Error(`${label} must be an object.`);
291
+ }
292
+
293
+ /*** Narrow an unknown value to a record. */
294
+ function isRecord(value: unknown): value is Record<string, unknown> {
295
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
296
+ }
297
+
298
+ /*** Require a non-empty string. */
299
+ function assertNonEmptyString(value: unknown, label: string): asserts value is string {
300
+ if (typeof value !== 'string' || value.trim() === '') {
301
+ throw new Error(`${label} must be a non-empty string.`);
302
+ }
303
+ }
304
+
305
+ /*** Run the portable recognition command when executed directly. */
306
+ async function main(): Promise<void> {
307
+ const [inputPath] = process.argv.slice(2);
308
+ if (!inputPath) {
309
+ throw new Error('Usage: analyze-screen.ts <screen-analysis-input.json>');
310
+ }
311
+ const input: unknown = JSON.parse(await readFile(inputPath, 'utf8'));
312
+ console.log(JSON.stringify(await analyzeScreenAsync(input), null, 2));
313
+ }
314
+
315
+ if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
316
+ main().catch((error) => {
317
+ console.error(error instanceof Error ? error.message : String(error));
318
+ process.exitCode = 1;
319
+ });
320
+ }
@@ -3,4 +3,4 @@ export declare const TEMPLATES_FIXTURE_SOURCE: string;
3
3
  export declare const COLOR_THEORY_FIXTURE_SOURCE = "\nexport const COLOR_HARMONIES = ['monochromatic', 'complementary'];\nexport const COLOR_HARMONY_CATALOG = [\n { id: 'monochromatic', label: 'Monochromatic', description: 'One hue.' },\n { id: 'complementary', label: 'Complementary', description: 'Opposing hues.' },\n];\n";
4
4
  export declare const CONTRACTS_FIXTURE_SOURCE = "\nexport const APP_CATEGORIES = ['business_productivity'];\nexport const NAVIGATOR_TYPES = ['stack', 'tabs', 'drawer'];\n";
5
5
  export declare const ZORA_THEME_FIXTURE_SOURCE = "\nexport const compileZoraTheme = (themeConfig) => ({\n themeConfig,\n light: { surfaceTheme: { mode: 'light' }, diagnostics: [] },\n dark: { surfaceTheme: { mode: 'dark' }, diagnostics: [] },\n diagnostics: [],\n});\n";
6
- export declare const ZORA_METADATA_FIXTURE_SOURCE = "\nexport const ZORA_COMPONENT_META = {\n View: { name: 'View', directManifestNode: true, allowedChildren: ['Text', 'Box'], props: {} },\n Box: { name: 'Box', directManifestNode: true, allowedChildren: [], props: {} },\n Text: {\n name: 'Text',\n directManifestNode: true,\n allowedChildren: [],\n props: { text: { type: 'string' } },\n events: {\n press: {\n eventType: 'text.press',\n label: 'Press',\n description: 'Text was pressed.',\n },\n },\n },\n MissingElement: {\n name: 'MissingElement',\n directManifestNode: true,\n allowedChildren: [],\n manifestPolicy: { kind: 'unresolved-element', availability: 'draft-only', releaseGate: 'blocked' },\n blueprint: { defaultProps: { requestedCapability: 'Unresolved', reason: 'No exact element.' } },\n props: {\n requestedCapability: { type: 'string' },\n reason: { type: 'string' },\n evidenceId: { type: 'string' },\n },\n },\n};\nexport const ZORA_CORE_PLUGIN_METADATA = {\n packageName: '@ankhorage/zora',\n componentMeta: ZORA_COMPONENT_META,\n extensionHosts: ['View', 'Box'],\n};\nexport const composeZoraPluginMetadata = (plugins) => {\n const componentMeta = {};\n const extensionHosts = new Set();\n for (const plugin of [...plugins].sort((left, right) => left.packageName.localeCompare(right.packageName))) {\n Object.assign(componentMeta, plugin.componentMeta);\n for (const host of plugin.extensionHosts ?? []) extensionHosts.add(host);\n }\n for (const plugin of plugins) {\n for (const placement of plugin.placements ?? []) {\n for (const parent of placement.parents) {\n if (!extensionHosts.has(parent)) throw new Error('Invalid extension host');\n componentMeta[parent] = {\n ...componentMeta[parent],\n allowedChildren: [...componentMeta[parent].allowedChildren, placement.child],\n };\n }\n }\n }\n return { componentMeta };\n};\nexport const ZORA_THEME_RECIPE_META = {\n Card: {\n name: 'Card',\n kind: 'component',\n fields: { variant: { type: 'choice', options: ['filled', 'outlined'] } },\n },\n};\n";
6
+ export declare const ZORA_METADATA_FIXTURE_SOURCE = "\nexport const ZORA_COMPONENT_META = {\n Screen: {\n name: 'Screen',\n category: 'layout',\n description: 'Screen layout root',\n directManifestNode: true,\n allowedChildren: ['View', 'Box', 'Text', 'MissingElement'],\n props: {},\n },\n View: {\n name: 'View',\n category: 'layout',\n directManifestNode: true,\n allowedChildren: ['Text', 'Box'],\n props: {},\n },\n Box: {\n name: 'Box',\n category: 'foundation',\n directManifestNode: true,\n allowedChildren: [],\n props: {},\n },\n Text: {\n name: 'Text',\n category: 'component',\n directManifestNode: true,\n allowedChildren: [],\n props: { text: { type: 'string' } },\n events: {\n press: {\n eventType: 'text.press',\n label: 'Press',\n description: 'Text was pressed.',\n },\n },\n },\n MissingElement: {\n name: 'MissingElement',\n category: 'pattern',\n directManifestNode: true,\n allowedChildren: [],\n manifestPolicy: { kind: 'unresolved-element', availability: 'draft-only', releaseGate: 'blocked' },\n blueprint: { defaultProps: { requestedCapability: 'Unresolved', reason: 'No exact element.' } },\n props: {\n requestedCapability: { type: 'string' },\n reason: { type: 'string' },\n evidenceId: { type: 'string' },\n },\n },\n};\nexport const ZORA_CORE_PLUGIN_METADATA = {\n packageName: '@ankhorage/zora',\n componentMeta: ZORA_COMPONENT_META,\n extensionHosts: ['Screen', 'View', 'Box'],\n};\nexport const composeZoraPluginMetadata = (plugins) => {\n const componentMeta = {};\n const extensionHosts = new Set();\n for (const plugin of [...plugins].sort((left, right) => left.packageName.localeCompare(right.packageName))) {\n Object.assign(componentMeta, plugin.componentMeta);\n for (const host of plugin.extensionHosts ?? []) extensionHosts.add(host);\n }\n for (const plugin of plugins) {\n for (const placement of plugin.placements ?? []) {\n for (const parent of placement.parents) {\n if (!extensionHosts.has(parent)) throw new Error('Invalid extension host');\n componentMeta[parent] = {\n ...componentMeta[parent],\n allowedChildren: [...componentMeta[parent].allowedChildren, placement.child],\n };\n }\n }\n }\n return { componentMeta };\n};\nexport const ZORA_THEME_RECIPE_META = {\n Card: {\n name: 'Card',\n kind: 'component',\n fields: { variant: { type: 'choice', options: ['filled', 'outlined'] } },\n },\n};\n";
@@ -81,10 +81,31 @@ export const compileZoraTheme = (themeConfig) => ({
81
81
  `;
82
82
  export const ZORA_METADATA_FIXTURE_SOURCE = `
83
83
  export const ZORA_COMPONENT_META = {
84
- View: { name: 'View', directManifestNode: true, allowedChildren: ['Text', 'Box'], props: {} },
85
- Box: { name: 'Box', directManifestNode: true, allowedChildren: [], props: {} },
84
+ Screen: {
85
+ name: 'Screen',
86
+ category: 'layout',
87
+ description: 'Screen layout root',
88
+ directManifestNode: true,
89
+ allowedChildren: ['View', 'Box', 'Text', 'MissingElement'],
90
+ props: {},
91
+ },
92
+ View: {
93
+ name: 'View',
94
+ category: 'layout',
95
+ directManifestNode: true,
96
+ allowedChildren: ['Text', 'Box'],
97
+ props: {},
98
+ },
99
+ Box: {
100
+ name: 'Box',
101
+ category: 'foundation',
102
+ directManifestNode: true,
103
+ allowedChildren: [],
104
+ props: {},
105
+ },
86
106
  Text: {
87
107
  name: 'Text',
108
+ category: 'component',
88
109
  directManifestNode: true,
89
110
  allowedChildren: [],
90
111
  props: { text: { type: 'string' } },
@@ -98,6 +119,7 @@ export const ZORA_COMPONENT_META = {
98
119
  },
99
120
  MissingElement: {
100
121
  name: 'MissingElement',
122
+ category: 'pattern',
101
123
  directManifestNode: true,
102
124
  allowedChildren: [],
103
125
  manifestPolicy: { kind: 'unresolved-element', availability: 'draft-only', releaseGate: 'blocked' },
@@ -112,7 +134,7 @@ export const ZORA_COMPONENT_META = {
112
134
  export const ZORA_CORE_PLUGIN_METADATA = {
113
135
  packageName: '@ankhorage/zora',
114
136
  componentMeta: ZORA_COMPONENT_META,
115
- extensionHosts: ['View', 'Box'],
137
+ extensionHosts: ['Screen', 'View', 'Box'],
116
138
  };
117
139
  export const composeZoraPluginMetadata = (plugins) => {
118
140
  const componentMeta = {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ankhorage/devtools",
3
- "version": "1.13.0",
3
+ "version": "1.14.1",
4
4
  "description": "Shared development tools and repository standards for Ankhorage",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/ankhorage/devtools#readme",
@@ -43,6 +43,9 @@
43
43
  "devtools.vscode.status"
44
44
  ]
45
45
  },
46
+ "ankhorage": {
47
+ "renovateSyncProtocol": 1
48
+ },
46
49
  "keywords": [
47
50
  "typescript",
48
51
  "eslint",
@@ -105,7 +108,7 @@
105
108
  "version-packages": "bun src/cli/bin/changeset.ts version"
106
109
  },
107
110
  "dependencies": {
108
- "@ankhorage/utility": "^0.3.0",
111
+ "@ankhorage/utility": "^0.5.0",
109
112
  "@changesets/cli": "^3.0.2",
110
113
  "@eslint/compat": "^2.1.1",
111
114
  "@eslint/js": "^10.0.1",
@@ -114,7 +117,7 @@
114
117
  "eslint-plugin-import": "^2.32.0",
115
118
  "eslint-plugin-prettier": "^5.5.6",
116
119
  "eslint-plugin-react": "^7.37.5",
117
- "eslint-plugin-react-hooks": "^7.1.1",
120
+ "eslint-plugin-react-hooks": "^7.0.0",
118
121
  "eslint-plugin-react-native": "^5.0.0",
119
122
  "eslint-plugin-security": "^4.0.1",
120
123
  "eslint-plugin-simple-import-sort": "^14.0.0",
@@ -126,8 +129,10 @@
126
129
  "devDependencies": {
127
130
  "@ankhorage/ankh": "^0.8.11",
128
131
  "@ankhorage/doctor": "0.10.8",
132
+ "@techstark/opencv-js": "^5.0.0-release.1",
129
133
  "@types/bun": "^1.4.1",
130
134
  "@types/node": "^26.4.1",
135
+ "sharp": "^0.35.4",
131
136
  "typescript": "~6.0.3"
132
137
  },
133
138
  "packageManager": "bun@1.4.2"