openuispec 0.1.25 → 0.1.28
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/README.md +63 -18
- package/cli/index.ts +21 -3
- package/cli/init.ts +27 -11
- package/docs/implementation-notes.md +119 -0
- package/docs/release-notes-v0.1.26.md +64 -0
- package/docs/release-notes-v0.1.27.md +28 -0
- package/docs/release-notes-v0.1.28.md +25 -0
- package/docs/stress-test-maturity-report.md +1 -1
- package/drift/index.ts +396 -22
- package/examples/taskflow/AGENTS.md +112 -0
- package/examples/taskflow/CLAUDE.md +112 -0
- package/examples/taskflow/generated/android/TaskFlow/README.md +43 -0
- package/examples/taskflow/generated/android/TaskFlow/app/build.gradle.kts +76 -0
- package/examples/taskflow/generated/android/TaskFlow/app/proguard-rules.pro +1 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/AndroidManifest.xml +21 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/MainActivity.kt +19 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/TaskFlowApp.kt +283 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/model/DomainModels.kt +106 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/model/SampleData.kt +57 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/components/Common.kt +109 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/HomeScreen.kt +112 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/ProjectsScreen.kt +61 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/SettingsScreen.kt +82 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/screens/TaskDetailScreen.kt +111 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/sheets/Sheets.kt +77 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Color.kt +30 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Theme.kt +86 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/java/uz/rsteam/taskflow/ui/theme/Type.kt +57 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/res/values/strings.xml +155 -0
- package/examples/taskflow/generated/android/TaskFlow/app/src/main/res/values/themes.xml +4 -0
- package/examples/taskflow/generated/android/TaskFlow/build.gradle.kts +5 -0
- package/examples/taskflow/generated/android/TaskFlow/gradle/gradle-daemon-jvm.properties +12 -0
- package/examples/taskflow/generated/android/TaskFlow/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/examples/taskflow/generated/android/TaskFlow/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/examples/taskflow/generated/android/TaskFlow/gradle.properties +4 -0
- package/examples/taskflow/generated/android/TaskFlow/gradlew +18 -0
- package/examples/taskflow/generated/android/TaskFlow/gradlew.bat +12 -0
- package/examples/taskflow/generated/android/TaskFlow/settings.gradle.kts +18 -0
- package/examples/taskflow/generated/ios/TaskFlow/README.md +21 -0
- package/examples/taskflow/generated/ios/TaskFlow/Resources/en.lproj/Localizable.strings +115 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/App/TaskFlowApp.swift +24 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Components/AppChrome.swift +150 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Flows/TaskEditorSheet.swift +220 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Models/DomainModels.swift +122 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/CalendarView.swift +21 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/HomeView.swift +201 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProfileEditView.swift +48 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProjectDetailView.swift +59 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/ProjectsView.swift +63 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/SettingsView.swift +85 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Screens/TaskDetailView.swift +219 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Support/AppModel.swift +320 -0
- package/examples/taskflow/generated/ios/TaskFlow/Sources/TaskFlow/Support/AppSupport.swift +41 -0
- package/examples/taskflow/generated/ios/TaskFlow/project.yml +26 -0
- package/examples/taskflow/generated/web/TaskFlow/README.md +19 -0
- package/examples/taskflow/generated/web/TaskFlow/index.html +12 -0
- package/examples/taskflow/generated/web/TaskFlow/package-lock.json +1908 -0
- package/examples/taskflow/generated/web/TaskFlow/package.json +24 -0
- package/examples/taskflow/generated/web/TaskFlow/src/App.tsx +58 -0
- package/examples/taskflow/generated/web/TaskFlow/src/AppShell.tsx +55 -0
- package/examples/taskflow/generated/web/TaskFlow/src/components/Common.tsx +82 -0
- package/examples/taskflow/generated/web/TaskFlow/src/components/Modals.tsx +191 -0
- package/examples/taskflow/generated/web/TaskFlow/src/components/Nav.tsx +41 -0
- package/examples/taskflow/generated/web/TaskFlow/src/generated/messages.ts +131 -0
- package/examples/taskflow/generated/web/TaskFlow/src/hooks.ts +25 -0
- package/examples/taskflow/generated/web/TaskFlow/src/i18n.ts +39 -0
- package/examples/taskflow/generated/web/TaskFlow/src/locales.en.json +111 -0
- package/examples/taskflow/generated/web/TaskFlow/src/main.tsx +13 -0
- package/examples/taskflow/generated/web/TaskFlow/src/screens/HomeScreen.tsx +111 -0
- package/examples/taskflow/generated/web/TaskFlow/src/screens/ProjectsScreen.tsx +82 -0
- package/examples/taskflow/generated/web/TaskFlow/src/screens/SettingsScreens.tsx +132 -0
- package/examples/taskflow/generated/web/TaskFlow/src/screens/TaskDetail.tsx +105 -0
- package/examples/taskflow/generated/web/TaskFlow/src/store.ts +216 -0
- package/examples/taskflow/generated/web/TaskFlow/src/styles.css +617 -0
- package/examples/taskflow/generated/web/TaskFlow/src/types.ts +64 -0
- package/examples/taskflow/generated/web/TaskFlow/src/utils.ts +78 -0
- package/examples/taskflow/generated/web/TaskFlow/tsconfig.json +21 -0
- package/examples/taskflow/generated/web/TaskFlow/vite.config.ts +6 -0
- package/examples/taskflow/openuispec/README.md +49 -0
- package/examples/todo-orbit/AGENTS.md +46 -14
- package/examples/todo-orbit/CLAUDE.md +46 -14
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/components/CommonComponents.kt +69 -18
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/screens/TasksScreen.kt +5 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/java/uz/rsteam/todoorbit/ui/sheets/EditorSheets.kt +5 -2
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/res/values/strings.xml +1 -0
- package/examples/todo-orbit/generated/android/Todo Orbit/app/src/main/res/values-ru/strings.xml +1 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Resources/en.lproj/Localizable.strings +1 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Resources/ru.lproj/Localizable.strings +1 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Flows/RecurringRuleSheet.swift +3 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Flows/TaskEditorSheet.swift +1 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/SettingsView.swift +2 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Screens/TasksHomeView.swift +1 -0
- package/examples/todo-orbit/generated/ios/Todo Orbit/Sources/TodoOrbit/Support/AppModel.swift +1 -1
- package/examples/todo-orbit/generated/web/Todo Orbit/src/App.tsx +59 -6
- package/examples/todo-orbit/generated/web/Todo Orbit/src/styles.css +40 -0
- package/examples/todo-orbit/openuispec/README.md +24 -131
- package/examples/todo-orbit/openuispec/flows/create_recurring_rule.yaml +3 -0
- package/examples/todo-orbit/openuispec/flows/create_task.yaml +1 -0
- package/examples/todo-orbit/openuispec/flows/edit_task.yaml +1 -0
- package/examples/todo-orbit/openuispec/locales/en.json +1 -0
- package/examples/todo-orbit/openuispec/locales/ru.json +1 -0
- package/examples/todo-orbit/openuispec/screens/task_detail.yaml +1 -0
- package/examples/todo-orbit/openuispec/tokens/icons.yaml +6 -0
- package/package.json +6 -1
- package/prepare/index.ts +391 -0
- package/schema/semantic-lint.ts +592 -0
- package/schema/validate.ts +17 -13
- package/status/index.ts +200 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/README.md +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/action_trigger.yaml +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/collection.yaml +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/data_display.yaml +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/feedback.yaml +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/input_field.yaml +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/nav_container.yaml +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/surface.yaml +0 -0
- /package/examples/taskflow/{contracts → openuispec/contracts}/x_media_player.yaml +0 -0
- /package/examples/taskflow/{flows → openuispec/flows}/create_task.yaml +0 -0
- /package/examples/taskflow/{flows → openuispec/flows}/edit_task.yaml +0 -0
- /package/examples/taskflow/{locales → openuispec/locales}/en.json +0 -0
- /package/examples/taskflow/{openuispec.yaml → openuispec/openuispec.yaml} +0 -0
- /package/examples/taskflow/{platform → openuispec/platform}/android.yaml +0 -0
- /package/examples/taskflow/{platform → openuispec/platform}/ios.yaml +0 -0
- /package/examples/taskflow/{platform → openuispec/platform}/web.yaml +0 -0
- /package/examples/taskflow/{screens → openuispec/screens}/calendar.yaml +0 -0
- /package/examples/taskflow/{screens → openuispec/screens}/home.yaml +0 -0
- /package/examples/taskflow/{screens → openuispec/screens}/profile_edit.yaml +0 -0
- /package/examples/taskflow/{screens → openuispec/screens}/project_detail.yaml +0 -0
- /package/examples/taskflow/{screens → openuispec/screens}/projects.yaml +0 -0
- /package/examples/taskflow/{screens → openuispec/screens}/settings.yaml +0 -0
- /package/examples/taskflow/{screens → openuispec/screens}/task_detail.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/color.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/elevation.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/icons.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/layout.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/motion.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/spacing.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/themes.yaml +0 -0
- /package/examples/taskflow/{tokens → openuispec/tokens}/typography.yaml +0 -0
package/drift/index.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* openuispec drift --target ios # check drift for ios
|
|
11
11
|
* openuispec drift # check all targets with snapshots
|
|
12
12
|
* openuispec drift --snapshot --target ios # snapshot for ios
|
|
13
|
+
* openuispec drift --target ios --explain # explain semantic changes since baseline
|
|
13
14
|
* openuispec drift --json --target ios # machine-readable output
|
|
14
15
|
* openuispec drift --target ios --all # include stubs in drift count
|
|
15
16
|
*/
|
|
@@ -17,6 +18,7 @@
|
|
|
17
18
|
import { readFileSync, writeFileSync, readdirSync, existsSync } from "node:fs";
|
|
18
19
|
import { resolve, join, relative, basename, dirname } from "node:path";
|
|
19
20
|
import { createHash } from "node:crypto";
|
|
21
|
+
import { execFileSync } from "node:child_process";
|
|
20
22
|
import YAML from "yaml";
|
|
21
23
|
|
|
22
24
|
const STATE_FILE = ".openuispec-state.json";
|
|
@@ -28,20 +30,47 @@ interface FileEntry {
|
|
|
28
30
|
status: string;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
interface StateFile {
|
|
33
|
+
export interface StateFile {
|
|
32
34
|
spec_version: string;
|
|
33
35
|
snapshot_at: string;
|
|
34
36
|
target: string;
|
|
37
|
+
baseline?: BaselineRef;
|
|
35
38
|
files: Record<string, FileEntry>;
|
|
36
39
|
}
|
|
37
40
|
|
|
38
|
-
interface
|
|
41
|
+
export interface BaselineRef {
|
|
42
|
+
kind: "git_commit" | "working_tree";
|
|
43
|
+
commit: string | null;
|
|
44
|
+
branch: string | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface DriftResult {
|
|
39
48
|
changed: string[];
|
|
40
49
|
added: string[];
|
|
41
50
|
removed: string[];
|
|
42
51
|
unchanged: string[];
|
|
43
52
|
}
|
|
44
53
|
|
|
54
|
+
export interface SemanticChange {
|
|
55
|
+
kind: "added" | "removed" | "changed";
|
|
56
|
+
path: string;
|
|
57
|
+
before?: string;
|
|
58
|
+
after?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface FileExplanation {
|
|
62
|
+
file: string;
|
|
63
|
+
status: "added" | "removed" | "changed";
|
|
64
|
+
changes: SemanticChange[];
|
|
65
|
+
truncated: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface ExplainResult {
|
|
69
|
+
available: boolean;
|
|
70
|
+
note?: string;
|
|
71
|
+
files: FileExplanation[];
|
|
72
|
+
}
|
|
73
|
+
|
|
45
74
|
// ── helpers ───────────────────────────────────────────────────────────
|
|
46
75
|
|
|
47
76
|
function listFiles(dir: string, ext: string): string[] {
|
|
@@ -61,6 +90,21 @@ function hashFile(filePath: string): string {
|
|
|
61
90
|
return `sha256:${hash}`;
|
|
62
91
|
}
|
|
63
92
|
|
|
93
|
+
function readFileIfExists(filePath: string): string | null {
|
|
94
|
+
try {
|
|
95
|
+
return readFileSync(filePath, "utf-8");
|
|
96
|
+
} catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function parseSpecDocument(relPath: string, content: string): unknown {
|
|
102
|
+
if (relPath.endsWith(".json")) {
|
|
103
|
+
return JSON.parse(content);
|
|
104
|
+
}
|
|
105
|
+
return YAML.parse(content);
|
|
106
|
+
}
|
|
107
|
+
|
|
64
108
|
/** Read the status field from a screen or flow YAML file. */
|
|
65
109
|
function readStatus(filePath: string): string {
|
|
66
110
|
try {
|
|
@@ -129,10 +173,249 @@ function categorize(relPath: string): string {
|
|
|
129
173
|
return "Other";
|
|
130
174
|
}
|
|
131
175
|
|
|
176
|
+
function runGit(args: string[], cwd: string): string | null {
|
|
177
|
+
try {
|
|
178
|
+
return execFileSync("git", args, {
|
|
179
|
+
cwd,
|
|
180
|
+
encoding: "utf-8",
|
|
181
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
182
|
+
}).trim();
|
|
183
|
+
} catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function gitPathForFile(projectDir: string, relPath: string): string | null {
|
|
189
|
+
const repoRoot = runGit(["rev-parse", "--show-toplevel"], projectDir);
|
|
190
|
+
if (!repoRoot) return null;
|
|
191
|
+
return relative(repoRoot, join(projectDir, relPath));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function readFileFromGit(projectDir: string, commit: string, relPath: string): string | null {
|
|
195
|
+
const gitPath = gitPathForFile(projectDir, relPath);
|
|
196
|
+
if (!gitPath) return null;
|
|
197
|
+
return runGit(["show", `${commit}:${gitPath}`], projectDir);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function captureBaseline(projectDir: string, files: string[]): BaselineRef | undefined {
|
|
201
|
+
const repoRoot = runGit(["rev-parse", "--show-toplevel"], projectDir);
|
|
202
|
+
if (!repoRoot) return undefined;
|
|
203
|
+
|
|
204
|
+
const branch = runGit(["branch", "--show-current"], projectDir);
|
|
205
|
+
const commit = runGit(["rev-parse", "HEAD"], projectDir);
|
|
206
|
+
const repoPaths = files.map((file) => relative(repoRoot, file));
|
|
207
|
+
const status = runGit(["status", "--porcelain", "--", ...repoPaths], projectDir) ?? "";
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
kind: status.length > 0 ? "working_tree" : "git_commit",
|
|
211
|
+
commit,
|
|
212
|
+
branch: branch || null,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function formatBaseline(baseline?: BaselineRef): string | null {
|
|
217
|
+
if (!baseline) return null;
|
|
218
|
+
|
|
219
|
+
const ref = baseline.commit ? baseline.commit.slice(0, 12) : "uncommitted";
|
|
220
|
+
const branchSuffix = baseline.branch ? ` on ${baseline.branch}` : "";
|
|
221
|
+
|
|
222
|
+
if (baseline.kind === "git_commit") {
|
|
223
|
+
return `${ref}${branchSuffix} (exact git baseline)`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return `${ref}${branchSuffix} + working tree spec changes`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const MAX_CHANGES_PER_FILE = 20;
|
|
230
|
+
const MAX_VALUE_LENGTH = 120;
|
|
231
|
+
|
|
232
|
+
function summarizeValue(value: unknown): string {
|
|
233
|
+
if (typeof value === "string") {
|
|
234
|
+
return value.length > MAX_VALUE_LENGTH
|
|
235
|
+
? JSON.stringify(`${value.slice(0, MAX_VALUE_LENGTH - 1)}…`)
|
|
236
|
+
: JSON.stringify(value);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const serialized = JSON.stringify(value);
|
|
240
|
+
if (!serialized) return String(value);
|
|
241
|
+
return serialized.length > MAX_VALUE_LENGTH
|
|
242
|
+
? `${serialized.slice(0, MAX_VALUE_LENGTH - 1)}…`
|
|
243
|
+
: serialized;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function compareSemanticValue(
|
|
247
|
+
path: string,
|
|
248
|
+
before: unknown,
|
|
249
|
+
after: unknown,
|
|
250
|
+
changes: SemanticChange[]
|
|
251
|
+
): void {
|
|
252
|
+
if (changes.length >= MAX_CHANGES_PER_FILE) return;
|
|
253
|
+
|
|
254
|
+
if (before === undefined && after === undefined) return;
|
|
255
|
+
if (before === undefined) {
|
|
256
|
+
changes.push({ kind: "added", path, after: summarizeValue(after) });
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (after === undefined) {
|
|
260
|
+
changes.push({ kind: "removed", path, before: summarizeValue(before) });
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (Array.isArray(before) || Array.isArray(after)) {
|
|
265
|
+
if (!Array.isArray(before) || !Array.isArray(after)) {
|
|
266
|
+
changes.push({
|
|
267
|
+
kind: "changed",
|
|
268
|
+
path,
|
|
269
|
+
before: summarizeValue(before),
|
|
270
|
+
after: summarizeValue(after),
|
|
271
|
+
});
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const maxLength = Math.max(before.length, after.length);
|
|
276
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
277
|
+
compareSemanticValue(`${path}[${index}]`, before[index], after[index], changes);
|
|
278
|
+
if (changes.length >= MAX_CHANGES_PER_FILE) return;
|
|
279
|
+
}
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (
|
|
284
|
+
before &&
|
|
285
|
+
after &&
|
|
286
|
+
typeof before === "object" &&
|
|
287
|
+
typeof after === "object"
|
|
288
|
+
) {
|
|
289
|
+
const beforeObj = before as Record<string, unknown>;
|
|
290
|
+
const afterObj = after as Record<string, unknown>;
|
|
291
|
+
const keys = Array.from(new Set([...Object.keys(beforeObj), ...Object.keys(afterObj)])).sort();
|
|
292
|
+
|
|
293
|
+
for (const key of keys) {
|
|
294
|
+
const nextPath = path ? `${path}.${key}` : key;
|
|
295
|
+
compareSemanticValue(nextPath, beforeObj[key], afterObj[key], changes);
|
|
296
|
+
if (changes.length >= MAX_CHANGES_PER_FILE) return;
|
|
297
|
+
}
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (before !== after) {
|
|
302
|
+
changes.push({
|
|
303
|
+
kind: "changed",
|
|
304
|
+
path,
|
|
305
|
+
before: summarizeValue(before),
|
|
306
|
+
after: summarizeValue(after),
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function explainFileChange(
|
|
312
|
+
projectDir: string,
|
|
313
|
+
baselineCommit: string,
|
|
314
|
+
relPath: string,
|
|
315
|
+
status: "added" | "removed" | "changed"
|
|
316
|
+
): FileExplanation {
|
|
317
|
+
if (status === "added") {
|
|
318
|
+
return {
|
|
319
|
+
file: relPath,
|
|
320
|
+
status,
|
|
321
|
+
changes: [{ kind: "added", path: relPath }],
|
|
322
|
+
truncated: false,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (status === "removed") {
|
|
327
|
+
return {
|
|
328
|
+
file: relPath,
|
|
329
|
+
status,
|
|
330
|
+
changes: [{ kind: "removed", path: relPath }],
|
|
331
|
+
truncated: false,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const beforeContent = readFileFromGit(projectDir, baselineCommit, relPath);
|
|
336
|
+
const afterContent = readFileIfExists(join(projectDir, relPath));
|
|
337
|
+
|
|
338
|
+
if (!beforeContent || !afterContent) {
|
|
339
|
+
return {
|
|
340
|
+
file: relPath,
|
|
341
|
+
status,
|
|
342
|
+
changes: [
|
|
343
|
+
{
|
|
344
|
+
kind: "changed",
|
|
345
|
+
path: relPath,
|
|
346
|
+
before: beforeContent ? "available" : "missing from baseline",
|
|
347
|
+
after: afterContent ? "available" : "missing from working tree",
|
|
348
|
+
},
|
|
349
|
+
],
|
|
350
|
+
truncated: false,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
const beforeDoc = parseSpecDocument(relPath, beforeContent);
|
|
356
|
+
const afterDoc = parseSpecDocument(relPath, afterContent);
|
|
357
|
+
const changes: SemanticChange[] = [];
|
|
358
|
+
compareSemanticValue("", beforeDoc, afterDoc, changes);
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
file: relPath,
|
|
362
|
+
status,
|
|
363
|
+
changes,
|
|
364
|
+
truncated: changes.length >= MAX_CHANGES_PER_FILE,
|
|
365
|
+
};
|
|
366
|
+
} catch (error) {
|
|
367
|
+
return {
|
|
368
|
+
file: relPath,
|
|
369
|
+
status,
|
|
370
|
+
changes: [
|
|
371
|
+
{
|
|
372
|
+
kind: "changed",
|
|
373
|
+
path: relPath,
|
|
374
|
+
after: error instanceof Error ? error.message : "unable to parse file diff",
|
|
375
|
+
},
|
|
376
|
+
],
|
|
377
|
+
truncated: false,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function explainDrift(projectDir: string, result: CheckResult): ExplainResult {
|
|
383
|
+
const baseline = result.state.baseline;
|
|
384
|
+
if (!baseline?.commit) {
|
|
385
|
+
return {
|
|
386
|
+
available: false,
|
|
387
|
+
note: "No git baseline metadata found in snapshot. Re-run `openuispec drift --snapshot --target <target>` from a git checkout.",
|
|
388
|
+
files: [],
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (baseline.kind !== "git_commit") {
|
|
393
|
+
return {
|
|
394
|
+
available: false,
|
|
395
|
+
note: "Snapshot was created from a dirty working tree, so semantic diff cannot reconstruct the exact baseline. Re-snapshot from a clean commit for precise explanations.",
|
|
396
|
+
files: [],
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const files: FileExplanation[] = [];
|
|
401
|
+
for (const relPath of result.drift.added) {
|
|
402
|
+
files.push(explainFileChange(projectDir, baseline.commit, relPath, "added"));
|
|
403
|
+
}
|
|
404
|
+
for (const relPath of result.drift.removed) {
|
|
405
|
+
files.push(explainFileChange(projectDir, baseline.commit, relPath, "removed"));
|
|
406
|
+
}
|
|
407
|
+
for (const relPath of result.drift.changed) {
|
|
408
|
+
files.push(explainFileChange(projectDir, baseline.commit, relPath, "changed"));
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
files.sort((a, b) => a.file.localeCompare(b.file));
|
|
412
|
+
return { available: true, files };
|
|
413
|
+
}
|
|
414
|
+
|
|
132
415
|
// ── project resolution ───────────────────────────────────────────────
|
|
133
416
|
|
|
134
417
|
/** Find the spec project directory by looking for openuispec.yaml. */
|
|
135
|
-
function findProjectDir(cwd: string): string {
|
|
418
|
+
export function findProjectDir(cwd: string): string {
|
|
136
419
|
const candidates = [
|
|
137
420
|
join(cwd, "openuispec"),
|
|
138
421
|
cwd,
|
|
@@ -152,7 +435,7 @@ function findProjectDir(cwd: string): string {
|
|
|
152
435
|
}
|
|
153
436
|
|
|
154
437
|
/** Read the project name from the manifest. */
|
|
155
|
-
function readProjectName(projectDir: string): string {
|
|
438
|
+
export function readProjectName(projectDir: string): string {
|
|
156
439
|
const doc = YAML.parse(
|
|
157
440
|
readFileSync(join(projectDir, "openuispec.yaml"), "utf-8")
|
|
158
441
|
);
|
|
@@ -160,7 +443,7 @@ function readProjectName(projectDir: string): string {
|
|
|
160
443
|
}
|
|
161
444
|
|
|
162
445
|
/** Read per-target output_dir map from the manifest. */
|
|
163
|
-
function readOutputDirs(projectDir: string): Record<string, string> {
|
|
446
|
+
export function readOutputDirs(projectDir: string): Record<string, string> {
|
|
164
447
|
try {
|
|
165
448
|
const doc = YAML.parse(readFileSync(join(projectDir, "openuispec.yaml"), "utf-8"));
|
|
166
449
|
return doc.generation?.output_dir ?? {};
|
|
@@ -170,7 +453,7 @@ function readOutputDirs(projectDir: string): Record<string, string> {
|
|
|
170
453
|
}
|
|
171
454
|
|
|
172
455
|
/** Resolve the generated output directory for a target. */
|
|
173
|
-
function resolveOutputDir(projectDir: string, projectName: string, target: string): string {
|
|
456
|
+
export function resolveOutputDir(projectDir: string, projectName: string, target: string): string {
|
|
174
457
|
const outputDirs = readOutputDirs(projectDir);
|
|
175
458
|
if (outputDirs[target]) {
|
|
176
459
|
return resolve(projectDir, outputDirs[target]);
|
|
@@ -179,11 +462,31 @@ function resolveOutputDir(projectDir: string, projectName: string, target: strin
|
|
|
179
462
|
return resolve(projectDir, "..", "generated", target, projectName);
|
|
180
463
|
}
|
|
181
464
|
|
|
182
|
-
function stateFilePath(projectDir: string, projectName: string, target: string): string {
|
|
465
|
+
export function stateFilePath(projectDir: string, projectName: string, target: string): string {
|
|
183
466
|
return join(resolveOutputDir(projectDir, projectName, target), STATE_FILE);
|
|
184
467
|
}
|
|
185
468
|
|
|
186
|
-
function
|
|
469
|
+
function missingSnapshotMessage(
|
|
470
|
+
cwd: string,
|
|
471
|
+
projectDir: string,
|
|
472
|
+
projectName: string,
|
|
473
|
+
target: string
|
|
474
|
+
): string {
|
|
475
|
+
const outDir = resolveOutputDir(projectDir, projectName, target);
|
|
476
|
+
if (!existsSync(outDir)) {
|
|
477
|
+
return (
|
|
478
|
+
`No snapshot found for target "${target}".\n` +
|
|
479
|
+
`Output directory not found: ${relative(cwd, outDir)}\n` +
|
|
480
|
+
`Run code generation for "${target}" first, then run: openuispec drift --snapshot --target ${target}`
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
return (
|
|
484
|
+
`No snapshot found for target "${target}".\n` +
|
|
485
|
+
`Run: openuispec drift --snapshot --target ${target}`
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export function discoverTargets(projectDir: string, projectName: string): string[] {
|
|
187
490
|
const outputDirs = readOutputDirs(projectDir);
|
|
188
491
|
const targets: string[] = [];
|
|
189
492
|
|
|
@@ -241,6 +544,7 @@ function snapshot(cwd: string, projectDir: string, target: string): void {
|
|
|
241
544
|
|
|
242
545
|
const files = discoverSpecFiles(projectDir);
|
|
243
546
|
const doc = YAML.parse(readFileSync(join(projectDir, "openuispec.yaml"), "utf-8"));
|
|
547
|
+
const baseline = captureBaseline(projectDir, files);
|
|
244
548
|
|
|
245
549
|
const entries: Record<string, FileEntry> = {};
|
|
246
550
|
let stubCount = 0;
|
|
@@ -256,6 +560,7 @@ function snapshot(cwd: string, projectDir: string, target: string): void {
|
|
|
256
560
|
spec_version: doc.spec_version ?? "0.1",
|
|
257
561
|
snapshot_at: new Date().toISOString(),
|
|
258
562
|
target,
|
|
563
|
+
baseline,
|
|
259
564
|
files: entries,
|
|
260
565
|
};
|
|
261
566
|
|
|
@@ -267,18 +572,23 @@ function snapshot(cwd: string, projectDir: string, target: string): void {
|
|
|
267
572
|
console.log(` ${stubCount} stubs (not tracked for drift)`);
|
|
268
573
|
}
|
|
269
574
|
console.log(` target: ${target}`);
|
|
575
|
+
const baselineLabel = formatBaseline(baseline);
|
|
576
|
+
if (baselineLabel) {
|
|
577
|
+
console.log(` baseline: ${baselineLabel}`);
|
|
578
|
+
}
|
|
270
579
|
}
|
|
271
580
|
|
|
272
581
|
// ── check ─────────────────────────────────────────────────────────────
|
|
273
582
|
|
|
274
|
-
interface CheckResult {
|
|
583
|
+
export interface CheckResult {
|
|
275
584
|
state: StateFile;
|
|
276
585
|
drift: DriftResult;
|
|
277
586
|
stubDrift: DriftResult;
|
|
278
587
|
statuses: Record<string, string>;
|
|
588
|
+
explanation?: ExplainResult;
|
|
279
589
|
}
|
|
280
590
|
|
|
281
|
-
function computeDrift(
|
|
591
|
+
export function computeDrift(
|
|
282
592
|
projectDir: string,
|
|
283
593
|
state: StateFile,
|
|
284
594
|
includeAll: boolean
|
|
@@ -326,25 +636,38 @@ function computeDrift(
|
|
|
326
636
|
return { state, drift, stubDrift, statuses };
|
|
327
637
|
}
|
|
328
638
|
|
|
329
|
-
function
|
|
639
|
+
export function loadTargetDrift(
|
|
330
640
|
cwd: string,
|
|
331
|
-
projectDir: string,
|
|
332
641
|
target: string,
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
):
|
|
642
|
+
includeAll: boolean,
|
|
643
|
+
explainOutput: boolean
|
|
644
|
+
): { projectDir: string; projectName: string; statePath: string; result: CheckResult } {
|
|
645
|
+
const projectDir = findProjectDir(cwd);
|
|
336
646
|
const projectName = readProjectName(projectDir);
|
|
337
647
|
const statePath = stateFilePath(projectDir, projectName, target);
|
|
338
648
|
if (!existsSync(statePath)) {
|
|
339
|
-
console.error(
|
|
340
|
-
`No snapshot found for target "${target}".\n` +
|
|
341
|
-
`Run: openuispec drift --snapshot --target ${target}`
|
|
342
|
-
);
|
|
649
|
+
console.error(missingSnapshotMessage(cwd, projectDir, projectName, target));
|
|
343
650
|
process.exit(1);
|
|
344
651
|
}
|
|
345
652
|
|
|
346
653
|
const state: StateFile = JSON.parse(readFileSync(statePath, "utf-8"));
|
|
347
654
|
const result = computeDrift(projectDir, state, includeAll);
|
|
655
|
+
if (explainOutput) {
|
|
656
|
+
result.explanation = explainDrift(projectDir, result);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return { projectDir, projectName, statePath, result };
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function check(
|
|
663
|
+
cwd: string,
|
|
664
|
+
projectDir: string,
|
|
665
|
+
target: string,
|
|
666
|
+
jsonOutput: boolean,
|
|
667
|
+
includeAll: boolean,
|
|
668
|
+
explainOutput: boolean
|
|
669
|
+
): void {
|
|
670
|
+
const { result } = loadTargetDrift(cwd, target, includeAll, explainOutput);
|
|
348
671
|
|
|
349
672
|
if (jsonOutput) {
|
|
350
673
|
printJson(result);
|
|
@@ -361,7 +684,8 @@ function checkAll(
|
|
|
361
684
|
cwd: string,
|
|
362
685
|
projectDir: string,
|
|
363
686
|
jsonOutput: boolean,
|
|
364
|
-
includeAll: boolean
|
|
687
|
+
includeAll: boolean,
|
|
688
|
+
explainOutput: boolean
|
|
365
689
|
): void {
|
|
366
690
|
const projectName = readProjectName(projectDir);
|
|
367
691
|
const targets = discoverTargets(projectDir, projectName);
|
|
@@ -378,6 +702,9 @@ function checkAll(
|
|
|
378
702
|
const statePath = stateFilePath(projectDir, projectName, target);
|
|
379
703
|
const state: StateFile = JSON.parse(readFileSync(statePath, "utf-8"));
|
|
380
704
|
const result = computeDrift(projectDir, state, includeAll);
|
|
705
|
+
if (explainOutput) {
|
|
706
|
+
result.explanation = explainDrift(projectDir, result);
|
|
707
|
+
}
|
|
381
708
|
|
|
382
709
|
if (jsonOutput) {
|
|
383
710
|
printJson(result);
|
|
@@ -411,7 +738,9 @@ function printJson(result: CheckResult): void {
|
|
|
411
738
|
{
|
|
412
739
|
snapshot_at: result.state.snapshot_at,
|
|
413
740
|
target: result.state.target,
|
|
741
|
+
baseline: result.state.baseline,
|
|
414
742
|
...result.drift,
|
|
743
|
+
explanation: result.explanation,
|
|
415
744
|
stubs: stubTotal > 0 ? result.stubDrift : undefined,
|
|
416
745
|
},
|
|
417
746
|
null,
|
|
@@ -428,6 +757,10 @@ function printReport(projectDir: string, result: CheckResult): void {
|
|
|
428
757
|
console.log(`Project: ${projectName}`);
|
|
429
758
|
console.log(`Snapshot: ${result.state.snapshot_at}`);
|
|
430
759
|
console.log(`Target: ${result.state.target}`);
|
|
760
|
+
const baselineLabel = formatBaseline(result.state.baseline);
|
|
761
|
+
if (baselineLabel) {
|
|
762
|
+
console.log(`Baseline: ${baselineLabel}`);
|
|
763
|
+
}
|
|
431
764
|
|
|
432
765
|
const d = result.drift;
|
|
433
766
|
|
|
@@ -500,6 +833,46 @@ function printReport(projectDir: string, result: CheckResult): void {
|
|
|
500
833
|
console.log(
|
|
501
834
|
`\nSummary: ${d.changed.length} changed, ${d.added.length} added, ${d.removed.length} removed${stubSuffix}`
|
|
502
835
|
);
|
|
836
|
+
|
|
837
|
+
if (result.explanation) {
|
|
838
|
+
console.log("\nSemantic Changes");
|
|
839
|
+
console.log("----------------");
|
|
840
|
+
|
|
841
|
+
if (!result.explanation.available) {
|
|
842
|
+
console.log(result.explanation.note ?? "Semantic explanation unavailable.");
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
if (result.explanation.files.length === 0) {
|
|
847
|
+
console.log("No semantic changes to explain.");
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
for (const file of result.explanation.files) {
|
|
852
|
+
console.log(`\n${file.file}`);
|
|
853
|
+
if (file.changes.length === 0) {
|
|
854
|
+
console.log(" · no property-level changes detected");
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
for (const change of file.changes) {
|
|
859
|
+
const pathLabel = change.path || "(root)";
|
|
860
|
+
if (change.kind === "added") {
|
|
861
|
+
const value = change.after ? ` = ${change.after}` : "";
|
|
862
|
+
console.log(` + ${pathLabel}${value}`);
|
|
863
|
+
} else if (change.kind === "removed") {
|
|
864
|
+
const value = change.before ? ` (was ${change.before})` : "";
|
|
865
|
+
console.log(` - ${pathLabel}${value}`);
|
|
866
|
+
} else {
|
|
867
|
+
console.log(` ~ ${pathLabel}: ${change.before ?? "?"} -> ${change.after ?? "?"}`);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
if (file.truncated) {
|
|
872
|
+
console.log(` … truncated after ${MAX_CHANGES_PER_FILE} changes`);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
503
876
|
}
|
|
504
877
|
|
|
505
878
|
// ── main ──────────────────────────────────────────────────────────────
|
|
@@ -508,6 +881,7 @@ export function runDrift(argv: string[]): void {
|
|
|
508
881
|
const isSnapshot = argv.includes("--snapshot");
|
|
509
882
|
const isJson = argv.includes("--json");
|
|
510
883
|
const includeAll = argv.includes("--all");
|
|
884
|
+
const explainOutput = argv.includes("--explain");
|
|
511
885
|
|
|
512
886
|
const targetIdx = argv.indexOf("--target");
|
|
513
887
|
const target = targetIdx !== -1 && argv[targetIdx + 1] ? argv[targetIdx + 1] : null;
|
|
@@ -523,9 +897,9 @@ export function runDrift(argv: string[]): void {
|
|
|
523
897
|
}
|
|
524
898
|
snapshot(cwd, projectDir, target);
|
|
525
899
|
} else if (target) {
|
|
526
|
-
check(cwd, projectDir, target, isJson, includeAll);
|
|
900
|
+
check(cwd, projectDir, target, isJson, includeAll, explainOutput);
|
|
527
901
|
} else {
|
|
528
|
-
checkAll(cwd, projectDir, isJson, includeAll);
|
|
902
|
+
checkAll(cwd, projectDir, isJson, includeAll, explainOutput);
|
|
529
903
|
}
|
|
530
904
|
}
|
|
531
905
|
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<!-- openuispec-rules-start -->
|
|
2
|
+
<!-- openuispec-rules-version: 0.1.28 -->
|
|
3
|
+
# OpenUISpec — AI Assistant Rules
|
|
4
|
+
# ================================
|
|
5
|
+
# This project uses OpenUISpec to define UI as a semantic spec.
|
|
6
|
+
# Spec files are the single source of truth for all UI across platforms.
|
|
7
|
+
# Targets: "ios", "android", "web"
|
|
8
|
+
|
|
9
|
+
## IMPORTANT — Read the specification before working with spec files
|
|
10
|
+
|
|
11
|
+
The spec format, file schemas, and generation rules are defined in the installed `openuispec` package.
|
|
12
|
+
You MUST read the reference files listed below before creating, editing, or generating from any spec file.
|
|
13
|
+
Do NOT guess the file format — skipping this step will produce invalid YAML that fails validation.
|
|
14
|
+
|
|
15
|
+
**Find the package in this order:**
|
|
16
|
+
1. `node_modules/openuispec/` (project dependency)
|
|
17
|
+
2. Run `npm root -g` → `<prefix>/openuispec/` (global install)
|
|
18
|
+
3. Online: `https://openuispec.rsteam.uz/llms-full.txt` (if not installed)
|
|
19
|
+
|
|
20
|
+
**Reference files inside the package (read in this order):**
|
|
21
|
+
1. `README.md` — schema tables, file format reference, root wrapper keys
|
|
22
|
+
2. `spec/openuispec-v0.1.md` — full specification (contracts, layout, expressions, adaptive, etc.)
|
|
23
|
+
3. `examples/taskflow/openuispec/` — complete working example with all file types
|
|
24
|
+
4. `schema/` — JSON Schemas for every file type
|
|
25
|
+
|
|
26
|
+
These files are updated with each package version. Always read from the installed package,
|
|
27
|
+
not from cached or memorized content, to ensure you use the latest spec.
|
|
28
|
+
|
|
29
|
+
## What is OpenUISpec
|
|
30
|
+
OpenUISpec is a YAML-based spec format that describes an app's UI semantically — tokens, screens, flows, and platform overrides. AI reads the spec and generates native code (SwiftUI, Compose, React). AI reads native code and updates the spec. The spec is the sync layer between platforms.
|
|
31
|
+
|
|
32
|
+
## Spec location
|
|
33
|
+
- Spec root: `openuispec/`
|
|
34
|
+
- Manifest: `openuispec/openuispec.yaml` — always read this first.
|
|
35
|
+
- Tokens: `openuispec/tokens/`
|
|
36
|
+
- Screens: `openuispec/screens/`
|
|
37
|
+
- Flows: `openuispec/flows/`
|
|
38
|
+
- Contracts: `openuispec/contracts/`
|
|
39
|
+
- Platform: `openuispec/platform/`
|
|
40
|
+
- Locales: `openuispec/locales/`
|
|
41
|
+
|
|
42
|
+
**Note:** These are the default paths. Actual paths are in `includes:` in `openuispec.yaml` and may use relative paths. Always read `openuispec.yaml` to find the real directories.
|
|
43
|
+
|
|
44
|
+
## If spec directories are empty (first-time setup)
|
|
45
|
+
This means the project has existing UI code but hasn't been specced yet. Your job:
|
|
46
|
+
|
|
47
|
+
1. **Read the spec first** — find and read `spec/openuispec-v0.1.md` from the installed package.
|
|
48
|
+
2. **Find existing screens** — scan the codebase for UI screen files.
|
|
49
|
+
3. **Create stubs** — for each screen, create `openuispec/screens/<name>.yaml` with:
|
|
50
|
+
```yaml
|
|
51
|
+
screen_name:
|
|
52
|
+
semantic: "Brief description of what this screen does"
|
|
53
|
+
status: stub
|
|
54
|
+
layout:
|
|
55
|
+
type: scroll_vertical
|
|
56
|
+
```
|
|
57
|
+
4. **Extract tokens** — scan for colors, fonts, spacing and create files in `openuispec/tokens/`.
|
|
58
|
+
5. **Update the manifest** — fill in `data_model` and `api.endpoints` in `openuispec/openuispec.yaml`.
|
|
59
|
+
|
|
60
|
+
## OpenUISpec Source Of Truth
|
|
61
|
+
|
|
62
|
+
OpenUISpec spec files are the primary source of truth for UI behavior across platforms.
|
|
63
|
+
|
|
64
|
+
### Start from spec when:
|
|
65
|
+
- the request changes screen structure
|
|
66
|
+
- the request changes navigation
|
|
67
|
+
- the request changes fields, actions, validation, or data binding
|
|
68
|
+
- the request changes tokens, variants, contracts, flows, or localization
|
|
69
|
+
- the request affects more than one platform
|
|
70
|
+
- the request is phrased in product/UI terms rather than platform-code terms
|
|
71
|
+
|
|
72
|
+
Spec-first workflow:
|
|
73
|
+
1. Read `openuispec/openuispec.yaml` and the relevant spec files first.
|
|
74
|
+
2. Update the spec first.
|
|
75
|
+
3. Update the affected generated/native UI code to match the spec.
|
|
76
|
+
4. Run `openuispec validate`.
|
|
77
|
+
5. Run `openuispec validate semantic`.
|
|
78
|
+
6. Run `openuispec drift --target <target> --explain` to inspect semantic changes since that target's baseline.
|
|
79
|
+
7. Run `openuispec prepare --target <target>` to build the AI/developer work bundle for that target.
|
|
80
|
+
8. Verify the affected UI targets build/run if possible.
|
|
81
|
+
9. Only then run `openuispec drift --snapshot --target <target>` for affected targets, after that target output directory exists.
|
|
82
|
+
10. Run `openuispec drift --target <target> --explain` again to confirm no spec changes remain for that target.
|
|
83
|
+
11. Use `openuispec status` to see which other targets are still behind the updated spec.
|
|
84
|
+
|
|
85
|
+
### Start from platform code when:
|
|
86
|
+
- the change is platform-specific polish
|
|
87
|
+
- the change is a local bug fix that does not alter shared semantic behavior
|
|
88
|
+
- the request explicitly asks for an iOS-only, Android-only, or web-only adjustment
|
|
89
|
+
|
|
90
|
+
Platform-first workflow:
|
|
91
|
+
1. Update native/platform code.
|
|
92
|
+
2. If the change affects shared semantics, sync the spec afterward.
|
|
93
|
+
3. If the change is intentionally platform-specific, document it in `platform/*.yaml` when appropriate.
|
|
94
|
+
|
|
95
|
+
### Never do this:
|
|
96
|
+
- Do not snapshot drift immediately after changing spec unless the UI code has also been updated.
|
|
97
|
+
- Do not treat `openuispec drift` as proof that generated UI matches the spec.
|
|
98
|
+
- Do not skip `--explain` / `prepare` when another platform needs to catch up with shared spec changes.
|
|
99
|
+
- Do not modify generated UI without checking whether the spec must change first.
|
|
100
|
+
|
|
101
|
+
## CLI commands
|
|
102
|
+
- `openuispec init` — scaffold a new spec project
|
|
103
|
+
- `openuispec validate [group...]` — validate spec files against schemas
|
|
104
|
+
- `openuispec validate semantic` — run semantic cross-reference linting
|
|
105
|
+
- `openuispec drift --target <t>` — check for spec drift
|
|
106
|
+
- `openuispec drift --target <t> --explain` — explain semantic spec drift since the target baseline
|
|
107
|
+
- `openuispec drift --snapshot --target <t>` — snapshot current state after the target output exists
|
|
108
|
+
- `openuispec prepare --target <t>` — build an AI-ready target update bundle
|
|
109
|
+
- `openuispec status` — show cross-target baseline/drift status
|
|
110
|
+
- `openuispec update-rules` — update AI rules to match installed package version
|
|
111
|
+
- `openuispec drift --all` — include stubs in drift check
|
|
112
|
+
<!-- openuispec-rules-end -->
|