openuispec 0.2.11 → 0.2.13
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 +2 -2
- package/check/index.ts +17 -0
- package/cli/index.ts +17 -1
- package/cli/init.ts +201 -2
- package/docs/file-formats.md +36 -0
- package/docs/implementation-notes.md +7 -0
- package/drift/index.ts +241 -26
- package/examples/taskflow/README.md +1 -1
- package/mcp-server/index.ts +25 -4
- package/package.json +1 -1
- package/prepare/index.ts +139 -18
- package/schema/custom-contract.schema.json +2 -2
- package/schema/openuispec.schema.json +71 -6
- package/schema/semantic-lint.ts +25 -1
- package/spec/openuispec-v0.1.md +22 -9
- package/status/index.ts +70 -0
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ This scaffolds a spec directory, starter tokens, and **configures the MCP server
|
|
|
46
46
|
## Key concepts
|
|
47
47
|
|
|
48
48
|
- **Tokens** — design values (color, typography, spacing, elevation, motion) with semantic names and constrained ranges
|
|
49
|
-
- **Contracts** — 7
|
|
49
|
+
- **Contracts** — 7 reusable UI component families defined by role, props, interaction states, and accessibility
|
|
50
50
|
- **Screens** — compositions of contracts with data bindings, adaptive layout, and conditional rendering
|
|
51
51
|
- **Flows** — multi-screen navigation journeys, intent-based and platform-agnostic
|
|
52
52
|
- **Actions** — 13 typed action types with composition, error handling, and optimistic updates
|
|
@@ -80,7 +80,7 @@ When you ask your AI to "add a settings page" or "update the home feed," the MCP
|
|
|
80
80
|
|
|
81
81
|
**Using without MCP?** You can provide spec context to any AI manually:
|
|
82
82
|
|
|
83
|
-
> Generate a native iOS app from this OpenUISpec. Follow all contract
|
|
83
|
+
> Generate a native iOS app from this OpenUISpec. Follow all contract UI states, apply token ranges for iOS, and implement navigation flows as defined.
|
|
84
84
|
|
|
85
85
|
## Examples
|
|
86
86
|
|
package/check/index.ts
CHANGED
|
@@ -14,10 +14,13 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
14
14
|
import { join, resolve } from "node:path";
|
|
15
15
|
import YAML from "yaml";
|
|
16
16
|
import {
|
|
17
|
+
computeSharedDrift,
|
|
17
18
|
findProjectDir,
|
|
19
|
+
hasDriftChanges,
|
|
18
20
|
readManifest,
|
|
19
21
|
readProjectName,
|
|
20
22
|
resolveOutputDir,
|
|
23
|
+
sharedLayersForTarget,
|
|
21
24
|
} from "../drift/index.js";
|
|
22
25
|
import {
|
|
23
26
|
buildAjv,
|
|
@@ -153,6 +156,20 @@ function determinePrepare(
|
|
|
153
156
|
);
|
|
154
157
|
}
|
|
155
158
|
|
|
159
|
+
// Check for shared layer drift (only when tracks are configured)
|
|
160
|
+
const sharedLayers = sharedLayersForTarget(projectDir, target);
|
|
161
|
+
for (const layer of sharedLayers) {
|
|
162
|
+
if (layer.tracks.length === 0) continue;
|
|
163
|
+
const driftResult = computeSharedDrift(projectDir, layer);
|
|
164
|
+
if (driftResult.state !== null) {
|
|
165
|
+
if (hasDriftChanges(driftResult.drift)) {
|
|
166
|
+
warnings.push(
|
|
167
|
+
`Shared layer "${layer.name}" has spec drift — shared code may need updates before ${target} generation.`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
156
173
|
const ready =
|
|
157
174
|
missing.length === 0 && backendContextReady && !pendingUserConfirmation;
|
|
158
175
|
|
package/cli/index.ts
CHANGED
|
@@ -95,6 +95,19 @@ function checkRulesVersion(): void {
|
|
|
95
95
|
|
|
96
96
|
// ── spec helpers (shared with MCP server) ────────────────────────────
|
|
97
97
|
|
|
98
|
+
function lookupLocaleKey(content: Record<string, unknown>, key: string): { found: boolean; value: unknown } {
|
|
99
|
+
if (key in content) return { found: true, value: content[key] };
|
|
100
|
+
const parts = key.split(".");
|
|
101
|
+
let current: unknown = content;
|
|
102
|
+
for (const part of parts) {
|
|
103
|
+
if (current === null || current === undefined || typeof current !== "object" || Array.isArray(current)) {
|
|
104
|
+
return { found: false, value: undefined };
|
|
105
|
+
}
|
|
106
|
+
current = (current as Record<string, unknown>)[part];
|
|
107
|
+
}
|
|
108
|
+
return current !== undefined ? { found: true, value: current } : { found: false, value: undefined };
|
|
109
|
+
}
|
|
110
|
+
|
|
98
111
|
function resolveSpecDir(projectDir: string, manifest: any, key: string): string {
|
|
99
112
|
return resolve(projectDir, manifest.includes?.[key] ?? `./${key}/`);
|
|
100
113
|
}
|
|
@@ -293,7 +306,10 @@ async function main(): Promise<void> {
|
|
|
293
306
|
const content = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
294
307
|
if (keys) {
|
|
295
308
|
const filtered: Record<string, unknown> = {};
|
|
296
|
-
for (const key of keys) {
|
|
309
|
+
for (const key of keys) {
|
|
310
|
+
const result = lookupLocaleKey(content, key);
|
|
311
|
+
if (result.found) filtered[key] = result.value;
|
|
312
|
+
}
|
|
297
313
|
console.log(JSON.stringify(filtered, null, 2));
|
|
298
314
|
} else {
|
|
299
315
|
console.log(JSON.stringify(content, null, 2));
|
package/cli/init.ts
CHANGED
|
@@ -33,6 +33,7 @@ export type InitOptionsResponse = {
|
|
|
33
33
|
options?: string[];
|
|
34
34
|
}>;
|
|
35
35
|
configure_targets_note: string;
|
|
36
|
+
shared_layer_note?: string;
|
|
36
37
|
};
|
|
37
38
|
|
|
38
39
|
export function listInitOptions(): InitOptionsResponse {
|
|
@@ -78,9 +79,17 @@ export function listInitOptions(): InitOptionsResponse {
|
|
|
78
79
|
type: "yes_no",
|
|
79
80
|
default: defaults.configureTargets,
|
|
80
81
|
},
|
|
82
|
+
{
|
|
83
|
+
key: "with_shared",
|
|
84
|
+
prompt: "Does the project share code between platforms (e.g. KMP commonMain)?",
|
|
85
|
+
type: "yes_no",
|
|
86
|
+
default: false,
|
|
87
|
+
},
|
|
81
88
|
],
|
|
82
89
|
configure_targets_note:
|
|
83
90
|
"If configure_targets is true, use `openuispec configure-target <target> --list-options` for each target after init to present stack choices to the user.",
|
|
91
|
+
shared_layer_note:
|
|
92
|
+
"If with_shared is true, add shared layer config to generation.shared in the manifest. Each shared layer needs: name, platforms (subset of targets), language, root (path relative to openuispec.yaml), tracks (spec categories: manifest, contracts, flows, screens, tokens, platform, locales), and scope (what code belongs there). Also add generation.structure entries for each target to define where platform-specific UI code goes and its scope.",
|
|
84
93
|
};
|
|
85
94
|
}
|
|
86
95
|
|
|
@@ -171,7 +180,12 @@ function getPackageVersion(): string {
|
|
|
171
180
|
function manifestTemplate(
|
|
172
181
|
name: string,
|
|
173
182
|
targets: string[],
|
|
174
|
-
options: {
|
|
183
|
+
options: {
|
|
184
|
+
withApi: boolean;
|
|
185
|
+
backendPath: string | null;
|
|
186
|
+
sharedLayers: SharedLayerAnswers[];
|
|
187
|
+
structures: StructureAnswers[];
|
|
188
|
+
}
|
|
175
189
|
): string {
|
|
176
190
|
const targetList = targets.join(", ");
|
|
177
191
|
const outputLines = targets
|
|
@@ -186,6 +200,38 @@ function manifestTemplate(
|
|
|
186
200
|
})
|
|
187
201
|
.join("\n");
|
|
188
202
|
|
|
203
|
+
function yamlPathsBlock(paths: Record<string, string>): string {
|
|
204
|
+
const entries = Object.entries(paths);
|
|
205
|
+
return entries.length > 0
|
|
206
|
+
? `\n paths:\n${entries.map(([k, v]) => ` ${k}: "${v}"`).join("\n")}`
|
|
207
|
+
: "";
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let sharedBlock = "";
|
|
211
|
+
if (options.sharedLayers.length > 0) {
|
|
212
|
+
const layers = options.sharedLayers.map((layer) => {
|
|
213
|
+
const tracksLine = layer.tracks.length > 0
|
|
214
|
+
? `\n tracks: [${layer.tracks.join(", ")}]`
|
|
215
|
+
: "";
|
|
216
|
+
return ` ${layer.name}:
|
|
217
|
+
platforms: [${layer.platforms.join(", ")}]
|
|
218
|
+
language: ${layer.language}
|
|
219
|
+
root: "${layer.root}"
|
|
220
|
+
scope: "${layer.scope}"${tracksLine}${yamlPathsBlock(layer.paths)}`;
|
|
221
|
+
}).join("\n");
|
|
222
|
+
sharedBlock = ` shared:\n${layers}\n`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let structureBlock = "";
|
|
226
|
+
if (options.structures.length > 0) {
|
|
227
|
+
const entries = options.structures.map((s) => {
|
|
228
|
+
const scopeLine = s.scope ? `\n scope: "${s.scope}"` : "";
|
|
229
|
+
return ` ${s.target}:
|
|
230
|
+
root: "${s.root}"${scopeLine}${yamlPathsBlock(s.paths)}`;
|
|
231
|
+
}).join("\n");
|
|
232
|
+
structureBlock = ` structure:\n${entries}\n`;
|
|
233
|
+
}
|
|
234
|
+
|
|
189
235
|
return `# ${name} — OpenUISpec v0.1
|
|
190
236
|
spec_version: "0.1"
|
|
191
237
|
|
|
@@ -218,7 +264,7 @@ ${options.withApi ? ` code_roots:
|
|
|
218
264
|
backend: "${options.backendPath}" # Required when api.endpoints are declared
|
|
219
265
|
` : ""} output_format:
|
|
220
266
|
${outputLines}
|
|
221
|
-
|
|
267
|
+
${sharedBlock}${structureBlock}
|
|
222
268
|
data_model: {}
|
|
223
269
|
|
|
224
270
|
api:
|
|
@@ -679,6 +725,23 @@ export function extractRulesVersion(filePath: string): string | null {
|
|
|
679
725
|
|
|
680
726
|
export { getPackageVersion };
|
|
681
727
|
|
|
728
|
+
interface SharedLayerAnswers {
|
|
729
|
+
name: string;
|
|
730
|
+
platforms: string[];
|
|
731
|
+
language: string;
|
|
732
|
+
root: string;
|
|
733
|
+
tracks: string[];
|
|
734
|
+
scope: string;
|
|
735
|
+
paths: Record<string, string>;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
interface StructureAnswers {
|
|
739
|
+
target: string;
|
|
740
|
+
root: string;
|
|
741
|
+
scope: string;
|
|
742
|
+
paths: Record<string, string>;
|
|
743
|
+
}
|
|
744
|
+
|
|
682
745
|
interface InitOptions {
|
|
683
746
|
defaults: boolean;
|
|
684
747
|
quiet: boolean;
|
|
@@ -688,6 +751,7 @@ interface InitOptions {
|
|
|
688
751
|
withApi?: boolean;
|
|
689
752
|
backendPath?: string;
|
|
690
753
|
configureTargets?: boolean;
|
|
754
|
+
withShared?: boolean;
|
|
691
755
|
}
|
|
692
756
|
|
|
693
757
|
interface InitAnswers {
|
|
@@ -697,6 +761,8 @@ interface InitAnswers {
|
|
|
697
761
|
withApi: boolean;
|
|
698
762
|
backendPath: string | null;
|
|
699
763
|
configureTargets: boolean;
|
|
764
|
+
sharedLayers: SharedLayerAnswers[];
|
|
765
|
+
structures: StructureAnswers[];
|
|
700
766
|
}
|
|
701
767
|
|
|
702
768
|
function parseTargetsValue(raw: string): string[] {
|
|
@@ -752,6 +818,12 @@ function parseInitArgs(argv: string[]): InitOptions {
|
|
|
752
818
|
case "--no-configure-targets":
|
|
753
819
|
options.configureTargets = false;
|
|
754
820
|
break;
|
|
821
|
+
case "--with-shared":
|
|
822
|
+
options.withShared = true;
|
|
823
|
+
break;
|
|
824
|
+
case "--no-shared":
|
|
825
|
+
options.withShared = false;
|
|
826
|
+
break;
|
|
755
827
|
default:
|
|
756
828
|
if (arg.startsWith("--")) {
|
|
757
829
|
console.error(`Error: Unknown init option: ${arg}`);
|
|
@@ -773,9 +845,127 @@ function collectDefaults(): InitAnswers {
|
|
|
773
845
|
withApi: true,
|
|
774
846
|
backendPath: "../backend/",
|
|
775
847
|
configureTargets: true,
|
|
848
|
+
sharedLayers: [],
|
|
849
|
+
structures: [],
|
|
776
850
|
};
|
|
777
851
|
}
|
|
778
852
|
|
|
853
|
+
const SHARED_LAYER_DEFAULTS: Record<string, {
|
|
854
|
+
language: string;
|
|
855
|
+
root: string;
|
|
856
|
+
tracks: string[];
|
|
857
|
+
scope: string;
|
|
858
|
+
paths: Record<string, string>;
|
|
859
|
+
structureScope: Record<string, string>;
|
|
860
|
+
}> = {
|
|
861
|
+
kmp: {
|
|
862
|
+
language: "kotlin",
|
|
863
|
+
root: "../shared",
|
|
864
|
+
tracks: [],
|
|
865
|
+
scope: "Business logic, data models, repositories, API clients, view models/stores. No UI rendering.",
|
|
866
|
+
paths: { domain: "commonMain/domain/", features: "commonMain/features/" },
|
|
867
|
+
structureScope: {
|
|
868
|
+
ios: "Pure SwiftUI views and navigation. All business logic comes from the shared layer.",
|
|
869
|
+
android: "Pure Compose UI and navigation. All business logic comes from the shared layer.",
|
|
870
|
+
},
|
|
871
|
+
},
|
|
872
|
+
};
|
|
873
|
+
|
|
874
|
+
function defaultSharedConfig(targets: string[]): {
|
|
875
|
+
sharedLayers: SharedLayerAnswers[];
|
|
876
|
+
structures: StructureAnswers[];
|
|
877
|
+
} {
|
|
878
|
+
const mobilePlatforms = targets.filter((t) => t === "ios" || t === "android");
|
|
879
|
+
if (mobilePlatforms.length < 2) {
|
|
880
|
+
return { sharedLayers: [], structures: [] };
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const preset = SHARED_LAYER_DEFAULTS.kmp;
|
|
884
|
+
const sharedLayers: SharedLayerAnswers[] = [{
|
|
885
|
+
name: "mobile_common",
|
|
886
|
+
platforms: mobilePlatforms,
|
|
887
|
+
language: preset.language,
|
|
888
|
+
root: preset.root,
|
|
889
|
+
tracks: preset.tracks,
|
|
890
|
+
scope: preset.scope,
|
|
891
|
+
paths: preset.paths,
|
|
892
|
+
}];
|
|
893
|
+
|
|
894
|
+
const structures: StructureAnswers[] = mobilePlatforms.map((t) => ({
|
|
895
|
+
target: t,
|
|
896
|
+
root: preset.root,
|
|
897
|
+
scope: preset.structureScope[t] ?? `Pure ${t} UI. Business logic comes from the shared layer.`,
|
|
898
|
+
paths: { ui: `${t}App/ui/` },
|
|
899
|
+
}));
|
|
900
|
+
|
|
901
|
+
return { sharedLayers, structures };
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
async function collectSharedLayerAnswers(
|
|
905
|
+
rl: ReturnType<typeof createInterface>,
|
|
906
|
+
targets: string[],
|
|
907
|
+
): Promise<{ sharedLayers: SharedLayerAnswers[]; structures: StructureAnswers[] }> {
|
|
908
|
+
const withShared = await askYesNo(rl, "\nShare code between platforms (e.g. KMP commonMain)?", false);
|
|
909
|
+
if (!withShared) return { sharedLayers: [], structures: [] };
|
|
910
|
+
|
|
911
|
+
const defaults = defaultSharedConfig(targets);
|
|
912
|
+
const defaultLayer = defaults.sharedLayers[0];
|
|
913
|
+
if (!defaultLayer) return { sharedLayers: [], structures: [] };
|
|
914
|
+
|
|
915
|
+
console.log("\n Shared layer defaults (KMP):");
|
|
916
|
+
console.log(` platforms: ${defaultLayer.platforms.join(", ")}`);
|
|
917
|
+
console.log(` language: ${defaultLayer.language}`);
|
|
918
|
+
console.log(` root: ${defaultLayer.root}`);
|
|
919
|
+
console.log(` scope: ${defaultLayer.scope}`);
|
|
920
|
+
|
|
921
|
+
const useDefaults = await askYesNo(rl, " Use these defaults?", true);
|
|
922
|
+
if (useDefaults) return defaults;
|
|
923
|
+
|
|
924
|
+
const layerName = await ask(rl, " Shared layer name", defaultLayer.name);
|
|
925
|
+
const platformsRaw = await askList(rl, " Platforms", targets, defaultLayer.platforms);
|
|
926
|
+
const language = await ask(rl, " Language", defaultLayer.language);
|
|
927
|
+
const root = await ask(rl, " Root path (relative to openuispec.yaml)", defaultLayer.root);
|
|
928
|
+
const scope = await ask(rl, " Scope (what code belongs here)", defaultLayer.scope);
|
|
929
|
+
const wantTracks = await askYesNo(rl, " Enable hash-based drift tracking for this layer?", false);
|
|
930
|
+
const tracksRaw = wantTracks
|
|
931
|
+
? await askList(
|
|
932
|
+
rl,
|
|
933
|
+
" Tracked spec categories",
|
|
934
|
+
["manifest", "tokens", "contracts", "screens", "flows", "platform", "locales"],
|
|
935
|
+
["manifest", "contracts", "flows"]
|
|
936
|
+
)
|
|
937
|
+
: [];
|
|
938
|
+
|
|
939
|
+
const sharedLayers: SharedLayerAnswers[] = [{
|
|
940
|
+
name: layerName,
|
|
941
|
+
platforms: platformsRaw,
|
|
942
|
+
language,
|
|
943
|
+
root,
|
|
944
|
+
tracks: tracksRaw,
|
|
945
|
+
scope,
|
|
946
|
+
paths: defaultLayer.paths,
|
|
947
|
+
}];
|
|
948
|
+
|
|
949
|
+
const structures: StructureAnswers[] = [];
|
|
950
|
+
for (const t of platformsRaw) {
|
|
951
|
+
const defaultStructure = defaults.structures.find((s) => s.target === t);
|
|
952
|
+
const structRoot = await ask(rl, ` ${t} structure root`, defaultStructure?.root ?? root);
|
|
953
|
+
const structScope = await ask(
|
|
954
|
+
rl,
|
|
955
|
+
` ${t} scope (what code belongs in the ${t} target)`,
|
|
956
|
+
defaultStructure?.scope ?? `Pure ${t} UI rendering.`
|
|
957
|
+
);
|
|
958
|
+
structures.push({
|
|
959
|
+
target: t,
|
|
960
|
+
root: structRoot,
|
|
961
|
+
scope: structScope,
|
|
962
|
+
paths: defaultStructure?.paths ?? { ui: `${t}App/ui/` },
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
return { sharedLayers, structures };
|
|
967
|
+
}
|
|
968
|
+
|
|
779
969
|
async function collectInteractiveAnswers(rl: ReturnType<typeof createInterface>): Promise<InitAnswers> {
|
|
780
970
|
const defaults = collectDefaults();
|
|
781
971
|
const name = await ask(rl, "Project name", defaults.name);
|
|
@@ -792,6 +982,7 @@ async function collectInteractiveAnswers(rl: ReturnType<typeof createInterface>)
|
|
|
792
982
|
? await ask(rl, "Backend folder path relative to openuispec.yaml", defaults.backendPath ?? "../backend/")
|
|
793
983
|
: null;
|
|
794
984
|
const configureTargets = await askYesNo(rl, "Configure target stacks now?", defaults.configureTargets);
|
|
985
|
+
const { sharedLayers, structures } = await collectSharedLayerAnswers(rl, targets);
|
|
795
986
|
|
|
796
987
|
return {
|
|
797
988
|
name,
|
|
@@ -800,6 +991,8 @@ async function collectInteractiveAnswers(rl: ReturnType<typeof createInterface>)
|
|
|
800
991
|
withApi,
|
|
801
992
|
backendPath,
|
|
802
993
|
configureTargets,
|
|
994
|
+
sharedLayers,
|
|
995
|
+
structures,
|
|
803
996
|
};
|
|
804
997
|
}
|
|
805
998
|
|
|
@@ -823,6 +1016,8 @@ function collectNonInteractiveAnswers(argv: string[]): InitAnswers {
|
|
|
823
1016
|
|
|
824
1017
|
const withApi = parsed.withApi ?? defaults.withApi;
|
|
825
1018
|
const backendPath = withApi ? parsed.backendPath ?? defaults.backendPath : null;
|
|
1019
|
+
const withShared = parsed.withShared ?? false;
|
|
1020
|
+
const { sharedLayers, structures } = withShared ? defaultSharedConfig(targets) : { sharedLayers: [], structures: [] };
|
|
826
1021
|
|
|
827
1022
|
return {
|
|
828
1023
|
name: parsed.name ?? defaults.name,
|
|
@@ -831,6 +1026,8 @@ function collectNonInteractiveAnswers(argv: string[]): InitAnswers {
|
|
|
831
1026
|
withApi,
|
|
832
1027
|
backendPath,
|
|
833
1028
|
configureTargets: parsed.configureTargets ?? defaults.configureTargets,
|
|
1029
|
+
sharedLayers,
|
|
1030
|
+
structures,
|
|
834
1031
|
};
|
|
835
1032
|
}
|
|
836
1033
|
|
|
@@ -879,6 +1076,8 @@ export async function init(argv: string[] = []): Promise<void> {
|
|
|
879
1076
|
manifestTemplate(answers.name, answers.targets, {
|
|
880
1077
|
withApi: answers.withApi,
|
|
881
1078
|
backendPath: answers.backendPath,
|
|
1079
|
+
sharedLayers: answers.sharedLayers,
|
|
1080
|
+
structures: answers.structures,
|
|
882
1081
|
}),
|
|
883
1082
|
quiet
|
|
884
1083
|
);
|
package/docs/file-formats.md
CHANGED
|
@@ -64,6 +64,42 @@ Paths are relative to `openuispec.yaml`. The `.openuispec-state.json` file recor
|
|
|
64
64
|
- `generation.extra_rules` can hold project-wide generation conventions
|
|
65
65
|
- `drift --snapshot` requires that target output directory to already exist
|
|
66
66
|
|
|
67
|
+
## Shared code layers
|
|
68
|
+
|
|
69
|
+
Projects that share business logic between platforms (e.g. KMP `commonMain`) can declare `generation.shared` to tell AI what code belongs in the shared layer vs platform-specific targets:
|
|
70
|
+
|
|
71
|
+
```yaml
|
|
72
|
+
generation:
|
|
73
|
+
targets: [ios, android, web]
|
|
74
|
+
shared:
|
|
75
|
+
mobile_common:
|
|
76
|
+
platforms: [ios, android]
|
|
77
|
+
language: kotlin
|
|
78
|
+
root: "../shared"
|
|
79
|
+
scope: "Business logic, data models, repositories, API clients, view models. No UI rendering."
|
|
80
|
+
# tracks: [manifest] # optional — enables hash-based drift detection for this layer
|
|
81
|
+
paths:
|
|
82
|
+
domain: "commonMain/domain/"
|
|
83
|
+
features: "commonMain/features/"
|
|
84
|
+
structure:
|
|
85
|
+
ios:
|
|
86
|
+
root: "../shared"
|
|
87
|
+
scope: "Pure SwiftUI views and navigation. All business logic comes from the shared layer."
|
|
88
|
+
paths:
|
|
89
|
+
ui: "iosApp/ui/"
|
|
90
|
+
android:
|
|
91
|
+
root: "../shared"
|
|
92
|
+
scope: "Pure Compose UI and navigation. All business logic comes from the shared layer."
|
|
93
|
+
paths:
|
|
94
|
+
ui: "androidApp/ui/"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
- **`scope`** (required on shared, optional on structure) — tells AI what code belongs where. This is the primary mechanism for routing generation work between shared and platform layers.
|
|
98
|
+
- **`tracks`** (optional) — when set, enables hash-based drift detection scoped to specific spec categories (`manifest`, `tokens`, `contracts`, `screens`, `flows`, `platform`, `locales`). When omitted, the shared layer relies on `scope` alone.
|
|
99
|
+
- **`structure`** — when present, overrides the heuristic code root discovery for a target. Paths are relative to `root`.
|
|
100
|
+
- Shared layers are not targets — they are tracked alongside targets in `prepare` and `status` output.
|
|
101
|
+
- `openuispec init --with-shared` scaffolds KMP defaults when both ios and android targets are selected.
|
|
102
|
+
|
|
67
103
|
## Spec sections overview
|
|
68
104
|
|
|
69
105
|
| Section | What it defines |
|
|
@@ -87,6 +87,13 @@
|
|
|
87
87
|
- backend generation context
|
|
88
88
|
- if the manifest declares `api.endpoints`, `generation.code_roots.backend` is required
|
|
89
89
|
- `prepare` should surface the resolved backend root so AI can inspect backend code when generating API clients
|
|
90
|
+
- Shared code layers (`generation.shared`):
|
|
91
|
+
- when configured, `prepare` includes `shared_layers` in its output with per-layer `scope`, `already_generated`, and `guidance`
|
|
92
|
+
- `scope` tells AI what code belongs in the shared layer vs the platform target — this is the primary routing mechanism
|
|
93
|
+
- optional `tracks` enables hash-based drift detection scoped to specific spec categories
|
|
94
|
+
- `generation.structure` overrides heuristic code root discovery when present, and its `scope` field tells AI what goes in the platform-specific target
|
|
95
|
+
- `suggestCodeRoots` includes shared layer roots and structure paths alongside target output directories
|
|
96
|
+
- generation rules include shared layer and target scope descriptions
|
|
90
97
|
- Important positioning:
|
|
91
98
|
- `prepare` does not generate code
|
|
92
99
|
- `prepare` does not verify code correctness
|