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/prepare/index.ts
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* AI preparation bundle for OpenUISpec projects.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* openuispec prepare --target ios # AI-ready work bundle for ios
|
|
7
|
+
* openuispec prepare --target web --json # machine-readable output
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
11
|
+
import { basename, extname, join, relative, resolve } from "node:path";
|
|
12
|
+
import YAML from "yaml";
|
|
13
|
+
import {
|
|
14
|
+
loadTargetDrift,
|
|
15
|
+
resolveOutputDir,
|
|
16
|
+
type FileExplanation,
|
|
17
|
+
type ExplainResult,
|
|
18
|
+
type SemanticChange,
|
|
19
|
+
} from "../drift/index.js";
|
|
20
|
+
|
|
21
|
+
interface PrepareItem {
|
|
22
|
+
spec_file: string;
|
|
23
|
+
category: string;
|
|
24
|
+
status: "added" | "removed" | "changed";
|
|
25
|
+
semantic_changes: SemanticChange[];
|
|
26
|
+
likely_files: string[];
|
|
27
|
+
notes: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface PrepareResult {
|
|
31
|
+
project: string;
|
|
32
|
+
target: string;
|
|
33
|
+
output_dir: string;
|
|
34
|
+
code_roots: string[];
|
|
35
|
+
baseline: {
|
|
36
|
+
kind: string | null;
|
|
37
|
+
commit: string | null;
|
|
38
|
+
branch: string | null;
|
|
39
|
+
};
|
|
40
|
+
summary: {
|
|
41
|
+
changed: number;
|
|
42
|
+
added: number;
|
|
43
|
+
removed: number;
|
|
44
|
+
};
|
|
45
|
+
changes_available: boolean;
|
|
46
|
+
explanation_note?: string;
|
|
47
|
+
items: PrepareItem[];
|
|
48
|
+
next_steps: string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readManifest(projectDir: string): Record<string, any> {
|
|
52
|
+
return YAML.parse(readFileSync(join(projectDir, "openuispec.yaml"), "utf-8"));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function categorizeSpecFile(relPath: string): string {
|
|
56
|
+
if (relPath === "openuispec.yaml") return "manifest";
|
|
57
|
+
const group = relPath.split("/")[0];
|
|
58
|
+
return group || "other";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function suggestCodeRoots(target: string, outputDir: string): string[] {
|
|
62
|
+
const candidates: string[] = [];
|
|
63
|
+
|
|
64
|
+
if (target === "web") {
|
|
65
|
+
candidates.push(join(outputDir, "src"), outputDir);
|
|
66
|
+
} else if (target === "ios") {
|
|
67
|
+
candidates.push(join(outputDir, "Sources"), join(outputDir, "Resources"), outputDir);
|
|
68
|
+
} else if (target === "android") {
|
|
69
|
+
candidates.push(
|
|
70
|
+
join(outputDir, "app", "src", "main", "java"),
|
|
71
|
+
join(outputDir, "app", "src", "main", "kotlin"),
|
|
72
|
+
join(outputDir, "app", "src", "main", "res"),
|
|
73
|
+
outputDir
|
|
74
|
+
);
|
|
75
|
+
} else {
|
|
76
|
+
candidates.push(outputDir);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const seen = new Set<string>();
|
|
80
|
+
return candidates
|
|
81
|
+
.map((candidate) => resolve(candidate))
|
|
82
|
+
.filter((candidate) => existsSync(candidate))
|
|
83
|
+
.filter((candidate) => {
|
|
84
|
+
if (seen.has(candidate)) return false;
|
|
85
|
+
seen.add(candidate);
|
|
86
|
+
return true;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function walkFiles(root: string, files: string[], depth = 0): void {
|
|
91
|
+
if (depth > 8) return;
|
|
92
|
+
|
|
93
|
+
for (const entry of readdirSync(root)) {
|
|
94
|
+
if (
|
|
95
|
+
entry === ".git" ||
|
|
96
|
+
entry === "node_modules" ||
|
|
97
|
+
entry === "build" ||
|
|
98
|
+
entry === "dist" ||
|
|
99
|
+
entry === ".gradle" ||
|
|
100
|
+
entry === "DerivedData"
|
|
101
|
+
) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const fullPath = join(root, entry);
|
|
106
|
+
let stat;
|
|
107
|
+
try {
|
|
108
|
+
stat = statSync(fullPath);
|
|
109
|
+
} catch {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (stat.isDirectory()) {
|
|
114
|
+
walkFiles(fullPath, files, depth + 1);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
files.push(fullPath);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isSearchableFile(filePath: string): boolean {
|
|
123
|
+
if (basename(filePath) === ".openuispec-state.json") return false;
|
|
124
|
+
const ext = extname(filePath).toLowerCase();
|
|
125
|
+
return new Set([
|
|
126
|
+
".ts",
|
|
127
|
+
".tsx",
|
|
128
|
+
".js",
|
|
129
|
+
".jsx",
|
|
130
|
+
".json",
|
|
131
|
+
".swift",
|
|
132
|
+
".kt",
|
|
133
|
+
".kts",
|
|
134
|
+
".xml",
|
|
135
|
+
".css",
|
|
136
|
+
".scss",
|
|
137
|
+
".md",
|
|
138
|
+
".plist",
|
|
139
|
+
".yaml",
|
|
140
|
+
".yml",
|
|
141
|
+
".strings",
|
|
142
|
+
]).has(ext);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function normalizeTerm(term: string): string | null {
|
|
146
|
+
const normalized = term.toLowerCase().replace(/[^a-z0-9._/-]+/g, "").trim();
|
|
147
|
+
if (!normalized || normalized.length < 3) return null;
|
|
148
|
+
if (["type", "props", "layout", "children", "title", "body", "root"].includes(normalized)) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
return normalized;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function buildSearchTerms(file: FileExplanation): string[] {
|
|
155
|
+
const terms = new Set<string>();
|
|
156
|
+
const stem = basename(file.file, extname(file.file));
|
|
157
|
+
const baseTerms = [
|
|
158
|
+
stem,
|
|
159
|
+
stem.replace(/_/g, ""),
|
|
160
|
+
stem.replace(/_/g, "."),
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
for (const term of baseTerms) {
|
|
164
|
+
const normalized = normalizeTerm(term);
|
|
165
|
+
if (normalized) terms.add(normalized);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
for (const change of file.changes) {
|
|
169
|
+
for (const part of change.path.split(/[.[\]/]+/)) {
|
|
170
|
+
const normalized = normalizeTerm(part);
|
|
171
|
+
if (normalized) terms.add(normalized);
|
|
172
|
+
}
|
|
173
|
+
const normalizedPath = normalizeTerm(change.path);
|
|
174
|
+
if (normalizedPath) terms.add(normalizedPath);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return Array.from(terms).sort((a, b) => b.length - a.length);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function searchLikelyFiles(outputDir: string, codeRoots: string[], file: FileExplanation): string[] {
|
|
181
|
+
const terms = buildSearchTerms(file);
|
|
182
|
+
if (terms.length === 0) return [];
|
|
183
|
+
|
|
184
|
+
const candidates: string[] = [];
|
|
185
|
+
for (const root of codeRoots) {
|
|
186
|
+
walkFiles(root, candidates);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const scored = candidates
|
|
190
|
+
.filter(isSearchableFile)
|
|
191
|
+
.map((candidate) => {
|
|
192
|
+
const relPath = relative(outputDir, candidate);
|
|
193
|
+
const pathScore = terms.reduce((sum, term) => sum + (relPath.toLowerCase().includes(term) ? 5 : 0), 0);
|
|
194
|
+
|
|
195
|
+
let contentScore = 0;
|
|
196
|
+
if (pathScore > 0 || terms.some((term) => term.includes("."))) {
|
|
197
|
+
try {
|
|
198
|
+
const text = readFileSync(candidate, "utf-8").toLowerCase();
|
|
199
|
+
contentScore = terms.reduce((sum, term) => sum + (text.includes(term) ? 2 : 0), 0);
|
|
200
|
+
} catch {
|
|
201
|
+
contentScore = 0;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { relPath, score: pathScore + contentScore };
|
|
206
|
+
})
|
|
207
|
+
.filter((entry) => entry.score > 0)
|
|
208
|
+
.sort((a, b) => b.score - a.score || a.relPath.localeCompare(b.relPath))
|
|
209
|
+
.slice(0, 12);
|
|
210
|
+
|
|
211
|
+
const unique = new Set<string>();
|
|
212
|
+
const results: string[] = [];
|
|
213
|
+
for (const entry of scored) {
|
|
214
|
+
if (unique.has(entry.relPath)) continue;
|
|
215
|
+
unique.add(entry.relPath);
|
|
216
|
+
results.push(entry.relPath);
|
|
217
|
+
if (results.length >= 6) break;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return results;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function buildCategoryNotes(category: string, target: string): string[] {
|
|
224
|
+
switch (category) {
|
|
225
|
+
case "screens":
|
|
226
|
+
return ["Update the target screen/view implementation and any matching navigation title or route shell."];
|
|
227
|
+
case "flows":
|
|
228
|
+
return ["Update target flow wiring, sheet/modal presentation, and action handlers for this flow."];
|
|
229
|
+
case "locales":
|
|
230
|
+
return ["Update target localization resources so new or changed locale keys are available at runtime."];
|
|
231
|
+
case "tokens":
|
|
232
|
+
return ["Update target theme, style, or shared visual tokens if the spec change affects appearance or spacing semantics."];
|
|
233
|
+
case "contracts":
|
|
234
|
+
return ["Update shared target primitives/renderers that realize this contract family."];
|
|
235
|
+
case "platform":
|
|
236
|
+
return [`Update ${target}-specific shell, navigation, or platform override behavior.`];
|
|
237
|
+
case "manifest":
|
|
238
|
+
return ["Recheck app shell, routing, data wiring, and generation target assumptions from the project manifest."];
|
|
239
|
+
default:
|
|
240
|
+
return ["Review the semantic diff and update the target implementation accordingly."];
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function explanationItems(
|
|
245
|
+
explanation: ExplainResult | undefined,
|
|
246
|
+
outputDir: string,
|
|
247
|
+
codeRoots: string[],
|
|
248
|
+
target: string
|
|
249
|
+
): PrepareItem[] {
|
|
250
|
+
if (!explanation?.available) return [];
|
|
251
|
+
|
|
252
|
+
return explanation.files.map((file) => ({
|
|
253
|
+
spec_file: file.file,
|
|
254
|
+
category: categorizeSpecFile(file.file),
|
|
255
|
+
status: file.status,
|
|
256
|
+
semantic_changes: file.changes,
|
|
257
|
+
likely_files: searchLikelyFiles(outputDir, codeRoots, file),
|
|
258
|
+
notes: buildCategoryNotes(categorizeSpecFile(file.file), target),
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function printReport(result: PrepareResult): void {
|
|
263
|
+
console.log("OpenUISpec Prepare");
|
|
264
|
+
console.log("==================");
|
|
265
|
+
console.log(`Project: ${result.project}`);
|
|
266
|
+
console.log(`Target: ${result.target}`);
|
|
267
|
+
console.log(`Output: ${result.output_dir}`);
|
|
268
|
+
if (result.baseline.commit) {
|
|
269
|
+
const shortCommit = result.baseline.commit.slice(0, 12);
|
|
270
|
+
const branch = result.baseline.branch ? ` on ${result.baseline.branch}` : "";
|
|
271
|
+
console.log(`Baseline: ${shortCommit}${branch} (${result.baseline.kind ?? "unknown"})`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
console.log(
|
|
275
|
+
`Summary: ${result.summary.changed} changed, ${result.summary.added} added, ${result.summary.removed} removed`
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
if (!result.changes_available) {
|
|
279
|
+
console.log(`\n${result.explanation_note ?? "No semantic changes available."}`);
|
|
280
|
+
} else if (result.items.length === 0) {
|
|
281
|
+
console.log("\nNo target updates are currently required from spec drift.");
|
|
282
|
+
} else {
|
|
283
|
+
console.log("\nCode Roots");
|
|
284
|
+
for (const root of result.code_roots) {
|
|
285
|
+
console.log(` - ${root}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
console.log("\nWork Items");
|
|
289
|
+
for (const item of result.items) {
|
|
290
|
+
console.log(`\n${item.spec_file}`);
|
|
291
|
+
for (const change of item.semantic_changes) {
|
|
292
|
+
const pathLabel = change.path || "(root)";
|
|
293
|
+
if (change.kind === "added") {
|
|
294
|
+
console.log(` + ${pathLabel}${change.after ? ` = ${change.after}` : ""}`);
|
|
295
|
+
} else if (change.kind === "removed") {
|
|
296
|
+
console.log(` - ${pathLabel}${change.before ? ` (was ${change.before})` : ""}`);
|
|
297
|
+
} else {
|
|
298
|
+
console.log(` ~ ${pathLabel}: ${change.before ?? "?"} -> ${change.after ?? "?"}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (item.likely_files.length > 0) {
|
|
303
|
+
console.log(" likely target files:");
|
|
304
|
+
for (const file of item.likely_files) {
|
|
305
|
+
console.log(` - ${file}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
for (const note of item.notes) {
|
|
310
|
+
console.log(` note: ${note}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
console.log("\nNext Steps");
|
|
316
|
+
for (const step of result.next_steps) {
|
|
317
|
+
console.log(` - ${step}`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function buildPrepareResult(target: string): PrepareResult {
|
|
322
|
+
const cwd = process.cwd();
|
|
323
|
+
const { projectDir, projectName, result } = loadTargetDrift(cwd, target, false, true);
|
|
324
|
+
const outputDir = resolveOutputDir(projectDir, projectName, target);
|
|
325
|
+
const codeRoots = suggestCodeRoots(target, outputDir);
|
|
326
|
+
const manifest = readManifest(projectDir);
|
|
327
|
+
const outputFormat = manifest.generation?.output_format?.[target] ?? {};
|
|
328
|
+
const items = explanationItems(result.explanation, outputDir, codeRoots, target);
|
|
329
|
+
|
|
330
|
+
const nextSteps = [
|
|
331
|
+
`Update the ${target} implementation in ${outputDir} to match the semantic changes above.`,
|
|
332
|
+
"Build or run the target and review the affected screens/flows.",
|
|
333
|
+
`After the UI is updated, run \`openuispec drift --snapshot --target ${target}\` to accept the new baseline.`,
|
|
334
|
+
`Run \`openuispec drift --target ${target} --explain\` again to confirm no spec changes remain for this target.`,
|
|
335
|
+
];
|
|
336
|
+
|
|
337
|
+
if (outputFormat.framework || outputFormat.language) {
|
|
338
|
+
nextSteps.unshift(
|
|
339
|
+
`Target mapping context: ${outputFormat.language ?? "unknown language"} / ${outputFormat.framework ?? "unknown framework"}.`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
project: projectName,
|
|
345
|
+
target,
|
|
346
|
+
output_dir: outputDir,
|
|
347
|
+
code_roots: codeRoots,
|
|
348
|
+
baseline: {
|
|
349
|
+
kind: result.state.baseline?.kind ?? null,
|
|
350
|
+
commit: result.state.baseline?.commit ?? null,
|
|
351
|
+
branch: result.state.baseline?.branch ?? null,
|
|
352
|
+
},
|
|
353
|
+
summary: {
|
|
354
|
+
changed: result.drift.changed.length,
|
|
355
|
+
added: result.drift.added.length,
|
|
356
|
+
removed: result.drift.removed.length,
|
|
357
|
+
},
|
|
358
|
+
changes_available: result.explanation?.available ?? false,
|
|
359
|
+
explanation_note: result.explanation?.note,
|
|
360
|
+
items,
|
|
361
|
+
next_steps: nextSteps,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function runPrepare(argv: string[]): void {
|
|
366
|
+
const isJson = argv.includes("--json");
|
|
367
|
+
const targetIdx = argv.indexOf("--target");
|
|
368
|
+
const target = targetIdx !== -1 && argv[targetIdx + 1] ? argv[targetIdx + 1] : null;
|
|
369
|
+
|
|
370
|
+
if (!target) {
|
|
371
|
+
console.error("Error: --target is required for prepare");
|
|
372
|
+
console.error("Usage: openuispec prepare --target <target> [--json]");
|
|
373
|
+
process.exit(1);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const result = buildPrepareResult(target);
|
|
377
|
+
if (isJson) {
|
|
378
|
+
console.log(JSON.stringify(result, null, 2));
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
printReport(result);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const isDirectRun =
|
|
386
|
+
process.argv[1]?.endsWith("prepare/index.ts") ||
|
|
387
|
+
process.argv[1]?.endsWith("prepare/index.js");
|
|
388
|
+
|
|
389
|
+
if (isDirectRun) {
|
|
390
|
+
runPrepare(process.argv.slice(2));
|
|
391
|
+
}
|