@sketchscreens/core-schema 0.1.0

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/schema.js ADDED
@@ -0,0 +1,231 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * The SketchScreens contract.
4
+ *
5
+ * One JSON shape, produced by the extractor and consumed by the renderer.
6
+ * It is deliberately stack-agnostic: a Next.js screen and a Flutter screen
7
+ * both reduce to the same `ScreenSpec`, so the renderer draws them identically.
8
+ *
9
+ * Design note: `ElementType` is a CLOSED enum on purpose. Each value maps 1:1
10
+ * onto a renderable hand-drawn primitive (mostly wired-elements). A closed set
11
+ * keeps every map renderable and stops the LLM extractor from inventing element
12
+ * types the renderer can't draw.
13
+ */
14
+ // ---------------------------------------------------------------------------
15
+ // Elements — the things that appear on a screen, in top-to-bottom order.
16
+ // ---------------------------------------------------------------------------
17
+ /** The closed set of wireframe primitives. Each maps to a renderable widget. */
18
+ export const ElementType = z.enum([
19
+ "heading", // a title / section header
20
+ "text", // a paragraph or helper/caption text
21
+ "input", // a single-line text field
22
+ "textarea", // a multi-line text field
23
+ "button", // a clickable button (may own a nav edge via its label)
24
+ "checkbox", // a single checkbox
25
+ "radio", // a radio option (usually one of a group)
26
+ "toggle", // an on/off switch
27
+ "select", // a dropdown / combo
28
+ "listbox", // a scrollable option list
29
+ "card", // a bordered container / tile
30
+ "image", // an image or avatar placeholder
31
+ "list", // a repeated row list (feeds, tables-as-list)
32
+ "divider", // a visual separator
33
+ "nav", // a nav bar / tab bar / menu
34
+ "tabs", // a tab strip
35
+ ]);
36
+ /** Optional visual role for buttons, so the renderer can vary emphasis. */
37
+ export const ElementVariant = z.enum([
38
+ "primary",
39
+ "secondary",
40
+ "destructive",
41
+ "ghost",
42
+ "link",
43
+ ]);
44
+ /**
45
+ * Which vertical band of the screen an element sits in. `top` = a header / nav
46
+ * bar pinned to the top; `bottom` = a footer / sticky action bar; `main` = the
47
+ * scrollable body (the default). Lets the sketch mirror the real screen's
48
+ * coarse layout without pixel positions.
49
+ */
50
+ export const ElementRegion = z.enum(["top", "main", "bottom"]);
51
+ /**
52
+ * Horizontal placement of an element within its band. `full` stretches edge to
53
+ * edge (inputs, nav bars, lists); `center` is a centered block (a hero button,
54
+ * a centered card); `left`/`right` hug a side. Default is `full` for fields and
55
+ * `left` for text.
56
+ */
57
+ export const ElementAlign = z.enum(["left", "center", "right", "full"]);
58
+ /**
59
+ * One UI element on a screen. `label` is the visible text (button caption,
60
+ * field label, heading text). Order in the `elements` array = top-to-bottom
61
+ * position in the sketch (structural fidelity, not pixel-exact).
62
+ */
63
+ export const ScreenElement = z.object({
64
+ type: ElementType,
65
+ /** Visible text: caption, label, or heading. Optional for dividers/images. */
66
+ label: z.string().optional(),
67
+ /** Placeholder / hint text for inputs. */
68
+ placeholder: z.string().optional(),
69
+ /** Visual emphasis (mainly for buttons). */
70
+ variant: ElementVariant.optional(),
71
+ /** True for password / masked inputs. */
72
+ secure: z.boolean().optional(),
73
+ /** True if the element is marked required in the source. */
74
+ required: z.boolean().optional(),
75
+ /**
76
+ * Optional grouping key. Elements sharing a group render inside one
77
+ * card/section, preserving the source's visual grouping.
78
+ */
79
+ group: z.string().optional(),
80
+ /**
81
+ * Which band of the screen this element sits in (top nav / main / bottom
82
+ * bar). Defaults to "main". Lets the sketch mirror the real layout.
83
+ */
84
+ region: ElementRegion.optional(),
85
+ /**
86
+ * Horizontal placement within the band. Defaults sensibly by type (fields
87
+ * full-width, text left). Set "center" for a centered hero button/card.
88
+ */
89
+ align: ElementAlign.optional(),
90
+ /**
91
+ * Whether `label` is the app's verbatim text or the extractor's SHAPE
92
+ * description of a dynamic region (a list/table it couldn't read literally).
93
+ * Defaults to "verbatim". The renderer styles "descriptive" labels distinctly
94
+ * so a reviewer sees "this is the tool's summary, not the app's words."
95
+ */
96
+ labelKind: z.enum(["verbatim", "descriptive"]).optional(),
97
+ /** Free-form note the extractor wants to surface (e.g. "conditionally shown"). */
98
+ note: z.string().optional(),
99
+ });
100
+ // ---------------------------------------------------------------------------
101
+ // Screens
102
+ // ---------------------------------------------------------------------------
103
+ /** One screen = one node in the map. */
104
+ export const ScreenSpec = z.object({
105
+ /** Stable id, unique within the map. Referenced by edges. */
106
+ id: z.string().min(1),
107
+ /** Human-friendly screen name (e.g. "Login"). */
108
+ name: z.string().min(1),
109
+ /**
110
+ * The route or address of this screen: a URL path (`/settings/profile`),
111
+ * a Flutter widget class (`SettingsScreen`), or an API path group. Optional
112
+ * because not every screen is addressable (e.g. a modal).
113
+ */
114
+ route: z.string().optional(),
115
+ /** Repo-relative path of the file this screen was extracted from. */
116
+ sourceFile: z.string().optional(),
117
+ /** The elements on this screen, in top-to-bottom order. */
118
+ elements: z.array(ScreenElement).default([]),
119
+ /** Optional short description / purpose of the screen. */
120
+ description: z.string().optional(),
121
+ /**
122
+ * Optional label-path for this screen's section, most-general first, using
123
+ * " › " as the separator — e.g. "Settings › AI Settings". Groups cluster and
124
+ * label screens; the renderer nests group labels. Deriveable from the route.
125
+ * (For the overall journey SHAPE, prefer `parent` below.)
126
+ */
127
+ group: z.string().optional(),
128
+ /**
129
+ * Optional id of the screen this one hangs beneath in the JOURNEY tree — the
130
+ * screen a user reaches this one *from*. This is what roots the map as a real
131
+ * flow: an entry screen has no parent; the auth screen's parent is the entry;
132
+ * each feature-section hub's parent is the dashboard; etc. When set, it
133
+ * overrides route-derived nesting so the tree matches how users actually
134
+ * navigate. Must reference another screen's `id`.
135
+ */
136
+ parent: z.string().optional(),
137
+ /**
138
+ * Optional hint that this screen is the app's entry point / root of the
139
+ * journey (what the user sees first). At most one screen should set this; the
140
+ * renderer roots the tree here. If none is set, the renderer infers a root.
141
+ */
142
+ isEntry: z.boolean().optional(),
143
+ /**
144
+ * How this screen is presented. "screen" (default) is a full page/route; the
145
+ * others are overlays. The renderer draws non-"screen" presentations with a
146
+ * distinct overlay frame and attaches them to their opener via an edge rather
147
+ * than the journey backbone.
148
+ */
149
+ presentation: z.enum(["screen", "modal", "drawer", "sheet"]).optional(),
150
+ /**
151
+ * Optional state variant this screen represents (an empty/error/loading view
152
+ * of another screen). The renderer can cluster it beside its base screen.
153
+ */
154
+ state: z.enum(["default", "empty", "error", "loading"]).optional(),
155
+ });
156
+ /** Split a group path ("Settings › AI Settings") into its segments. */
157
+ export function groupSegments(group) {
158
+ if (!group)
159
+ return [];
160
+ return group
161
+ .split("›")
162
+ .map((s) => s.trim())
163
+ .filter(Boolean);
164
+ }
165
+ // ---------------------------------------------------------------------------
166
+ // Edges — navigation between screens
167
+ // ---------------------------------------------------------------------------
168
+ /** A navigation transition from one screen to another. */
169
+ export const Edge = z.object({
170
+ /** Source screen id. */
171
+ from: z.string().min(1),
172
+ /** Destination screen id. */
173
+ to: z.string().min(1),
174
+ /**
175
+ * What triggers the transition — usually a button/link label
176
+ * ("Continue", "Forgot?"). Rendered as the edge label.
177
+ */
178
+ trigger: z.string().optional(),
179
+ /** How the transition is expressed, if useful (e.g. "redirect", "push", "link"). */
180
+ kind: z.string().optional(),
181
+ });
182
+ // ---------------------------------------------------------------------------
183
+ // The map
184
+ // ---------------------------------------------------------------------------
185
+ /**
186
+ * Which surface this map represents. Each surface is its own map — a web app,
187
+ * a mobile app, a CRM, and a backend are mapped separately, not tangled into one.
188
+ */
189
+ export const Surface = z.enum([
190
+ "web",
191
+ "mobile",
192
+ "crm",
193
+ "admin",
194
+ "backend",
195
+ "desktop",
196
+ "other",
197
+ ]);
198
+ /** Optional provenance / metadata about how this map was produced. */
199
+ export const ProjectMapMeta = z.object({
200
+ /** The tool version that produced the map. */
201
+ generator: z.string().optional(),
202
+ /** Repo-relative or absolute root the map was extracted from. */
203
+ repoRoot: z.string().optional(),
204
+ /** ISO timestamp string (set by the producer; schema stays time-agnostic). */
205
+ generatedAt: z.string().optional(),
206
+ /** How elements were derived: "agent", "static", or "hybrid". */
207
+ extraction: z.enum(["agent", "static", "hybrid", "manual"]).optional(),
208
+ });
209
+ /** The contract version this build of SketchScreens speaks. */
210
+ export const CONTRACT_VERSION = 1;
211
+ /** The whole thing: one surface's screens + the flow between them. */
212
+ export const ProjectMap = z.object({
213
+ /**
214
+ * Contract version. Accepts any positive integer so an OLD renderer reading a
215
+ * NEWER map fails with a clear "upgrade" message (see validate.ts) rather than
216
+ * a generic zod "invalid literal". New fields are additive; bump only on a
217
+ * breaking change.
218
+ */
219
+ version: z.number().int().min(1).default(CONTRACT_VERSION),
220
+ /** Display name for this map (e.g. "AiPhone 360 — Web App"). */
221
+ name: z.string().min(1),
222
+ /** Which surface this represents. */
223
+ surface: Surface,
224
+ /** The screens (nodes). */
225
+ screens: z.array(ScreenSpec).default([]),
226
+ /** The navigation transitions (edges). */
227
+ edges: z.array(Edge).default([]),
228
+ /** Optional provenance. */
229
+ meta: ProjectMapMeta.optional(),
230
+ });
231
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import { ProjectMap } from "./schema.js";
2
+ /** How serious a validation issue is. Errors fail the gate; warnings don't. */
3
+ export type IssueSeverity = "error" | "warning";
4
+ /** A validation problem found in a ProjectMap. */
5
+ export interface ValidationIssue {
6
+ /** Machine-readable code. */
7
+ code: "schema" | "duplicate_screen_id" | "edge_unknown_from" | "edge_unknown_to" | "self_edge" | "parent_unknown" | "parent_self" | "parent_cycle" | "no_screens" | "entry_count" | "duplicate_edge" | "sourcefile_missing" | "unsupported_version";
8
+ /** Error (fails the gate) or warning (renders anyway). Defaults to error. */
9
+ severity: IssueSeverity;
10
+ /** Human-readable message. */
11
+ message: string;
12
+ /** JSON-ish path to the offending value, when known. */
13
+ path?: string;
14
+ }
15
+ export interface ValidationResult {
16
+ /** True when there are no ERROR-severity issues (warnings don't fail it). */
17
+ ok: boolean;
18
+ /** The parsed map when the shape is valid (defaults applied), else undefined. */
19
+ map?: ProjectMap;
20
+ issues: ValidationIssue[];
21
+ }
22
+ /**
23
+ * Validate a raw object as a ProjectMap.
24
+ *
25
+ * Runs the zod schema first (shape + enums + defaults), then referential
26
+ * checks that zod can't express: unique screen ids and edges that point at
27
+ * screens that actually exist. This is the gate the extractor's output must
28
+ * pass before the renderer will draw it.
29
+ */
30
+ export declare function validateProjectMap(input: unknown): ValidationResult;
31
+ /**
32
+ * Like {@link validateProjectMap} but throws on failure. Useful in scripts
33
+ * and tests where an invalid map should hard-stop.
34
+ */
35
+ export declare function parseProjectMap(input: unknown): ProjectMap;
36
+ //# sourceMappingURL=validate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAoB,MAAM,aAAa,CAAC;AAE3D,+EAA+E;AAC/E,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,SAAS,CAAC;AAEhD,kDAAkD;AAClD,MAAM,WAAW,eAAe;IAC9B,6BAA6B;IAC7B,IAAI,EAEA,QAAQ,GACR,qBAAqB,GACrB,mBAAmB,GACnB,iBAAiB,GACjB,WAAW,GACX,gBAAgB,GAChB,aAAa,GACb,cAAc,GAEd,YAAY,GACZ,aAAa,GACb,gBAAgB,GAChB,oBAAoB,GACpB,qBAAqB,CAAC;IAC1B,6EAA6E;IAC7E,QAAQ,EAAE,aAAa,CAAC;IACxB,8BAA8B;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,wDAAwD;IACxD,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B,6EAA6E;IAC7E,EAAE,EAAE,OAAO,CAAC;IACZ,iFAAiF;IACjF,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,gBAAgB,CAqGnE;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,UAAU,CAS1D"}
@@ -0,0 +1,111 @@
1
+ import { ProjectMap, CONTRACT_VERSION } from "./schema.js";
2
+ /**
3
+ * Validate a raw object as a ProjectMap.
4
+ *
5
+ * Runs the zod schema first (shape + enums + defaults), then referential
6
+ * checks that zod can't express: unique screen ids and edges that point at
7
+ * screens that actually exist. This is the gate the extractor's output must
8
+ * pass before the renderer will draw it.
9
+ */
10
+ export function validateProjectMap(input) {
11
+ const parsed = ProjectMap.safeParse(input);
12
+ if (!parsed.success) {
13
+ const issues = parsed.error.issues.map((i) => ({
14
+ code: "schema",
15
+ severity: "error",
16
+ message: i.message,
17
+ path: i.path.join("."),
18
+ }));
19
+ return { ok: false, issues };
20
+ }
21
+ const map = parsed.data;
22
+ const issues = [];
23
+ const err = (code, message, path) => issues.push({ code, severity: "error", message, path });
24
+ const warn = (code, message, path) => issues.push({ code, severity: "warning", message, path });
25
+ // A map from a newer contract version — render what we can, but flag it.
26
+ if (map.version > CONTRACT_VERSION) {
27
+ warn("unsupported_version", `Map is contract version ${map.version} but this SketchScreens speaks ${CONTRACT_VERSION} — some things may not render. Upgrade SketchScreens.`);
28
+ }
29
+ // A map with no screens renders as a blank canvas — warn rather than crash.
30
+ if (map.screens.length === 0) {
31
+ warn("no_screens", "The map has no screens.");
32
+ }
33
+ // Unique screen ids.
34
+ const seen = new Set();
35
+ for (const screen of map.screens) {
36
+ if (seen.has(screen.id)) {
37
+ err("duplicate_screen_id", `Duplicate screen id "${screen.id}".`, `screens[id=${screen.id}]`);
38
+ }
39
+ seen.add(screen.id);
40
+ }
41
+ // At most one entry screen; ideally exactly one (the renderer infers if 0).
42
+ const entryCount = map.screens.filter((s) => s.isEntry).length;
43
+ if (entryCount > 1) {
44
+ warn("entry_count", `${entryCount} screens are marked isEntry; there should be exactly one (the app's first screen).`);
45
+ }
46
+ // Edges must reference existing screens, and not point a screen at itself.
47
+ const edgeKeys = new Set();
48
+ map.edges.forEach((edge, idx) => {
49
+ if (!seen.has(edge.from)) {
50
+ err("edge_unknown_from", `Edge #${idx} references unknown source screen "${edge.from}".`, `edges[${idx}].from`);
51
+ }
52
+ if (!seen.has(edge.to)) {
53
+ err("edge_unknown_to", `Edge #${idx} references unknown target screen "${edge.to}".`, `edges[${idx}].to`);
54
+ }
55
+ if (edge.from === edge.to) {
56
+ err("self_edge", `Edge #${idx} points screen "${edge.from}" at itself.`, `edges[${idx}]`);
57
+ }
58
+ const key = `${edge.from}->${edge.to}`;
59
+ if (edgeKeys.has(key)) {
60
+ warn("duplicate_edge", `Edge #${idx} duplicates ${key}.`, `edges[${idx}]`);
61
+ }
62
+ edgeKeys.add(key);
63
+ });
64
+ // Journey `parent` links: must reference an existing screen, not self, and
65
+ // must not form a cycle (walking parents from any screen must terminate).
66
+ const parentOf = new Map();
67
+ for (const screen of map.screens) {
68
+ if (screen.parent === undefined)
69
+ continue;
70
+ if (screen.parent === screen.id) {
71
+ err("parent_self", `Screen "${screen.id}" lists itself as its parent.`, `screens[id=${screen.id}].parent`);
72
+ continue;
73
+ }
74
+ if (!seen.has(screen.parent)) {
75
+ err("parent_unknown", `Screen "${screen.id}" has unknown parent "${screen.parent}".`, `screens[id=${screen.id}].parent`);
76
+ continue;
77
+ }
78
+ parentOf.set(screen.id, screen.parent);
79
+ }
80
+ // Cycle detection over the parent chains.
81
+ for (const start of parentOf.keys()) {
82
+ const visited = new Set([start]);
83
+ let cur = parentOf.get(start);
84
+ while (cur !== undefined) {
85
+ if (visited.has(cur)) {
86
+ err("parent_cycle", `Parent chain from "${start}" forms a cycle at "${cur}".`, `screens[id=${start}].parent`);
87
+ break;
88
+ }
89
+ visited.add(cur);
90
+ cur = parentOf.get(cur);
91
+ }
92
+ }
93
+ // The gate fails only on ERROR-severity issues; warnings render anyway.
94
+ const ok = !issues.some((i) => i.severity === "error");
95
+ return { ok, map, issues };
96
+ }
97
+ /**
98
+ * Like {@link validateProjectMap} but throws on failure. Useful in scripts
99
+ * and tests where an invalid map should hard-stop.
100
+ */
101
+ export function parseProjectMap(input) {
102
+ const result = validateProjectMap(input);
103
+ if (!result.ok || !result.map) {
104
+ const detail = result.issues
105
+ .map((i) => ` - [${i.code}] ${i.message}${i.path ? ` (${i.path})` : ""}`)
106
+ .join("\n");
107
+ throw new Error(`Invalid ProjectMap:\n${detail}`);
108
+ }
109
+ return result.map;
110
+ }
111
+ //# sourceMappingURL=validate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.js","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAwC3D;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAc;IAC/C,MAAM,MAAM,GAAG,UAAU,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC3C,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,MAAM,GAAsB,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAChE,IAAI,EAAE,QAAQ;YACd,QAAQ,EAAE,OAAO;YACjB,OAAO,EAAE,CAAC,CAAC,OAAO;YAClB,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;SACvB,CAAC,CAAC,CAAC;QACJ,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAC/B,CAAC;IAED,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC;IACxB,MAAM,MAAM,GAAsB,EAAE,CAAC;IACrC,MAAM,GAAG,GAAG,CAAC,IAA6B,EAAE,OAAe,EAAE,IAAa,EAAE,EAAE,CAC5E,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;IAC1D,MAAM,IAAI,GAAG,CAAC,IAA6B,EAAE,OAAe,EAAE,IAAa,EAAE,EAAE,CAC7E,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;IAE5D,yEAAyE;IACzE,IAAI,GAAG,CAAC,OAAO,GAAG,gBAAgB,EAAE,CAAC;QACnC,IAAI,CACF,qBAAqB,EACrB,2BAA2B,GAAG,CAAC,OAAO,kCAAkC,gBAAgB,uDAAuD,CAChJ,CAAC;IACJ,CAAC;IAED,4EAA4E;IAC5E,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC7B,IAAI,CAAC,YAAY,EAAE,yBAAyB,CAAC,CAAC;IAChD,CAAC;IAED,qBAAqB;IACrB,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,KAAK,MAAM,MAAM,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC;QACjC,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC;YACxB,GAAG,CAAC,qBAAqB,EAAE,wBAAwB,MAAM,CAAC,EAAE,IAAI,EAAE,cAAc,MAAM,CAAC,EAAE,GAAG,CAAC,CAAC;QAChG,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACtB,CAAC;IAED,4EAA4E;IAC5E,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC;IAC/D,IAAI,UAAU,GAAG,CAAC,EAAE,CAAC;QACnB,IAAI,CACF,aAAa,EACb,GAAG,UAAU,oFAAoF,CAClG,CAAC;IACJ,CAAC;IAED,2EAA2E;IAC3E,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;IACnC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;QAC9B,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACzB,GAAG,CAAC,mBAAmB,EAAE,SAAS,GAAG,sCAAsC,IAAI,CAAC,IAAI,IAAI,EAAE,SAAS,GAAG,QAAQ,CAAC,CAAC;QAClH,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;YACvB,GAAG,CAAC,iBAAiB,EAAE,SAAS,GAAG,sCAAsC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,GAAG,MAAM,CAAC,CAAC;QAC5G,CAAC;QACD,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,EAAE,EAAE,CAAC;YAC1B,GAAG,CAAC,WAAW,EAAE,SAAS,GAAG,mBAAmB,IAAI,CAAC,IAAI,cAAc,EAAE,SAAS,GAAG,GAAG,CAAC,CAAC;QAC5F,CAAC;QACD,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,EAAE,EAAE,CAAC;QACvC,IAAI,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YACtB,IAAI,CAAC,gBAAgB,EAAE,SAAS,GAAG,eAAe,GAAG,GAAG,EAAE,SAAS,GAAG,GAAG,CAAC,CAAC;QAC7E,CAAC;QACD,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACpB,CAAC,CAAC,CAAC;IAEH,2EAA2E;IAC3E,0EAA0E;IAC1E,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC3C,KAAK,MAAM,MAAM,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC;QACjC,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS;YAAE,SAAS;QAC1C,IAAI,MAAM,CAAC,MAAM,KAAK,MAAM,CAAC,EAAE,EAAE,CAAC;YAChC,GAAG,CAAC,aAAa,EAAE,WAAW,MAAM,CAAC,EAAE,+BAA+B,EAAE,cAAc,MAAM,CAAC,EAAE,UAAU,CAAC,CAAC;YAC3G,SAAS;QACX,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;YAC7B,GAAG,CAAC,gBAAgB,EAAE,WAAW,MAAM,CAAC,EAAE,yBAAyB,MAAM,CAAC,MAAM,IAAI,EAAE,cAAc,MAAM,CAAC,EAAE,UAAU,CAAC,CAAC;YACzH,SAAS;QACX,CAAC;QACD,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;IACzC,CAAC;IACD,0CAA0C;IAC1C,KAAK,MAAM,KAAK,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC;QACpC,MAAM,OAAO,GAAG,IAAI,GAAG,CAAS,CAAC,KAAK,CAAC,CAAC,CAAC;QACzC,IAAI,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAC9B,OAAO,GAAG,KAAK,SAAS,EAAE,CAAC;YACzB,IAAI,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;gBACrB,GAAG,CAAC,cAAc,EAAE,sBAAsB,KAAK,uBAAuB,GAAG,IAAI,EAAE,cAAc,KAAK,UAAU,CAAC,CAAC;gBAC9G,MAAM;YACR,CAAC;YACD,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACjB,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC1B,CAAC;IACH,CAAC;IAED,wEAAwE;IACxE,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC;IACvD,OAAO,EAAE,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC;AAC7B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,MAAM,MAAM,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC;IACzC,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC;QAC9B,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM;aACzB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;aACzE,IAAI,CAAC,IAAI,CAAC,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,wBAAwB,MAAM,EAAE,CAAC,CAAC;IACpD,CAAC;IACD,OAAO,MAAM,CAAC,GAAG,CAAC;AACpB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@sketchscreens/core-schema",
3
+ "version": "0.1.0",
4
+ "description": "The ProjectMap / ScreenSpec contract — the stack-agnostic shape that the extractor produces and the renderer consumes.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "main": "./dist/index.js",
11
+ "module": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js"
17
+ },
18
+ "./audit": {
19
+ "types": "./dist/audit.d.ts",
20
+ "import": "./dist/audit.js"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "src"
26
+ ],
27
+ "dependencies": {
28
+ "zod": "^3.24.1"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^22.0.0",
32
+ "tsx": "^4.19.2",
33
+ "typescript": "^5.7.2"
34
+ },
35
+ "scripts": {
36
+ "build": "tsc -p tsconfig.json",
37
+ "typecheck": "tsc -p tsconfig.json --noEmit",
38
+ "test": "node --test --import tsx test/*.test.ts"
39
+ }
40
+ }
package/src/audit.ts ADDED
@@ -0,0 +1,67 @@
1
+ import { existsSync, statSync } from "node:fs";
2
+ import { isAbsolute, resolve } from "node:path";
3
+ import type { ProjectMap } from "./schema.js";
4
+ import type { ValidationIssue } from "./validate.js";
5
+
6
+ /**
7
+ * NODE-ONLY provenance audit. Separate from validate.ts (which stays
8
+ * browser-safe) because this touches the filesystem — only the CLI imports it.
9
+ *
10
+ * Verifies each screen's `sourceFile` actually exists under the repo root, so a
11
+ * hallucinated path can't masquerade as real provenance. Missing files are
12
+ * WARNINGS, not errors: a modal/dialog screen may legitimately have no file.
13
+ */
14
+
15
+ export interface AuditResult {
16
+ /** Repo root the sourceFiles were resolved against. */
17
+ repoRoot: string;
18
+ /** How many screens declared a sourceFile. */
19
+ withSourceFile: number;
20
+ /** How many of those resolved to a real file. */
21
+ resolved: number;
22
+ /** sourceFiles that didn't resolve (repo-relative as written). */
23
+ missing: string[];
24
+ /** Warning issues (one per missing sourceFile). */
25
+ issues: ValidationIssue[];
26
+ }
27
+
28
+ /**
29
+ * Audit a map's `sourceFile`s against `repoRoot`. Pass the root from the map's
30
+ * `meta.repoRoot` (or the cwd the extraction ran in). `~` is expanded.
31
+ */
32
+ export function auditSourceFiles(map: ProjectMap, repoRoot: string): AuditResult {
33
+ const root = expandHome(repoRoot);
34
+ const missing: string[] = [];
35
+ const issues: ValidationIssue[] = [];
36
+ let withSourceFile = 0;
37
+ let resolved = 0;
38
+
39
+ for (const screen of map.screens) {
40
+ const sf = screen.sourceFile;
41
+ if (!sf) continue;
42
+ withSourceFile++;
43
+ const abs = isAbsolute(sf) ? sf : resolve(root, sf);
44
+ if (existsSync(abs) && statSync(abs).isFile()) {
45
+ resolved++;
46
+ } else {
47
+ missing.push(sf);
48
+ issues.push({
49
+ code: "sourcefile_missing",
50
+ severity: "warning",
51
+ message: `Screen "${screen.id}" sourceFile does not exist: ${sf}`,
52
+ path: `screens[id=${screen.id}].sourceFile`,
53
+ });
54
+ }
55
+ }
56
+
57
+ return { repoRoot: root, withSourceFile, resolved, missing, issues };
58
+ }
59
+
60
+ /** Expand a leading ~ to the user's home directory. */
61
+ function expandHome(p: string): string {
62
+ if (p === "~" || p.startsWith("~/")) {
63
+ const home = process.env.HOME || process.env.USERPROFILE || "";
64
+ return home + p.slice(1);
65
+ }
66
+ return p;
67
+ }
package/src/index.ts ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * @sketchscreens/core-schema
3
+ *
4
+ * The contract at the center of SketchScreens: the `ProjectMap` shape that the
5
+ * extractor produces and the renderer consumes. Import the zod schemas to
6
+ * validate, or the inferred types to build maps in TypeScript.
7
+ */
8
+
9
+ export {
10
+ ElementType,
11
+ ElementVariant,
12
+ ElementRegion,
13
+ ElementAlign,
14
+ ScreenElement,
15
+ ScreenSpec,
16
+ Edge,
17
+ Surface,
18
+ ProjectMapMeta,
19
+ ProjectMap,
20
+ groupSegments,
21
+ } from "./schema.js";
22
+
23
+ export type {
24
+ ElementType as ElementTypeT,
25
+ ElementVariant as ElementVariantT,
26
+ ElementRegion as ElementRegionT,
27
+ ElementAlign as ElementAlignT,
28
+ ScreenElement as ScreenElementT,
29
+ ScreenSpec as ScreenSpecT,
30
+ Edge as EdgeT,
31
+ Surface as SurfaceT,
32
+ ProjectMapMeta as ProjectMapMetaT,
33
+ ProjectMap as ProjectMapT,
34
+ } from "./schema.js";
35
+
36
+ export {
37
+ validateProjectMap,
38
+ parseProjectMap,
39
+ type ValidationIssue,
40
+ type ValidationResult,
41
+ type IssueSeverity,
42
+ } from "./validate.js";
43
+
44
+ // NOTE: the filesystem audit (auditSourceFiles) lives in ./audit.js and is
45
+ // intentionally NOT re-exported here — it imports node:fs and would pollute the
46
+ // browser bundle. CLIs import it via the "@sketchscreens/core-schema/audit"
47
+ // subpath export (see package.json).