@origostudio/origo-design 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/.editorconfig +13 -0
  2. package/.gitattributes +22 -0
  3. package/.perf-baseline.json +12 -0
  4. package/.prettierignore +13 -0
  5. package/.prettierrc +10 -0
  6. package/.vscode/extensions.json +3 -0
  7. package/BRANCH-PROTECTION.md +25 -0
  8. package/CHANGELOG.md +292 -0
  9. package/LICENSE +21 -0
  10. package/README.md +11 -0
  11. package/commitlint.config.js +6 -0
  12. package/dist/packages/cli/CHANGELOG.md +92 -0
  13. package/dist/packages/cli/README.md +11 -0
  14. package/dist/packages/cli/package.json +16 -0
  15. package/dist/packages/cli/package.json.bak +16 -0
  16. package/dist/packages/cli/src/commands/generate/entity.d.ts +2 -0
  17. package/dist/packages/cli/src/commands/generate/entity.js +24 -0
  18. package/dist/packages/cli/src/commands/generate/entity.js.map +1 -0
  19. package/dist/packages/cli/src/commands/generate/index.d.ts +2 -0
  20. package/dist/packages/cli/src/commands/generate/index.js +30 -0
  21. package/dist/packages/cli/src/commands/generate/index.js.map +1 -0
  22. package/dist/packages/cli/src/commands/init.d.ts +5 -0
  23. package/dist/packages/cli/src/commands/init.js +27 -0
  24. package/dist/packages/cli/src/commands/init.js.map +1 -0
  25. package/dist/packages/cli/src/commands/new.d.ts +2 -0
  26. package/dist/packages/cli/src/commands/new.js +24 -0
  27. package/dist/packages/cli/src/commands/new.js.map +1 -0
  28. package/dist/packages/cli/src/commands/validate.d.ts +2 -0
  29. package/dist/packages/cli/src/commands/validate.js +50 -0
  30. package/dist/packages/cli/src/commands/validate.js.map +1 -0
  31. package/dist/packages/cli/src/index.d.ts +11 -0
  32. package/dist/packages/cli/src/index.js +15 -0
  33. package/dist/packages/cli/src/index.js.map +1 -0
  34. package/dist/packages/cli/src/lib/cli.d.ts +1 -0
  35. package/dist/packages/cli/src/lib/cli.js +7 -0
  36. package/dist/packages/cli/src/lib/cli.js.map +1 -0
  37. package/dist/packages/cli/src/lib/generator.d.ts +16 -0
  38. package/dist/packages/cli/src/lib/generator.js +165 -0
  39. package/dist/packages/cli/src/lib/generator.js.map +1 -0
  40. package/dist/packages/cli/src/lib/scaffolding.d.ts +5 -0
  41. package/dist/packages/cli/src/lib/scaffolding.js +87 -0
  42. package/dist/packages/cli/src/lib/scaffolding.js.map +1 -0
  43. package/dist/packages/cli/src/lib/validation.d.ts +4 -0
  44. package/dist/packages/cli/src/lib/validation.js +191 -0
  45. package/dist/packages/cli/src/lib/validation.js.map +1 -0
  46. package/dist/packages/cli/src/main.d.ts +4 -0
  47. package/dist/packages/cli/src/main.js +38 -0
  48. package/dist/packages/cli/src/main.js.map +1 -0
  49. package/dist/packages/cli/src/templates/entity.json +6 -0
  50. package/dist/packages/cli/src/templates/extension.json +8 -0
  51. package/dist/packages/cli/src/templates/index.d.ts +29 -0
  52. package/dist/packages/cli/src/templates/index.js +61 -0
  53. package/dist/packages/cli/src/templates/index.js.map +1 -0
  54. package/dist/packages/cli/src/templates/origo.json +16 -0
  55. package/dist/packages/cli/src/utils/errors.d.ts +15 -0
  56. package/dist/packages/cli/src/utils/errors.js +58 -0
  57. package/dist/packages/cli/src/utils/errors.js.map +1 -0
  58. package/dist/packages/core/CHANGELOG.md +275 -0
  59. package/dist/packages/core/README.md +11 -0
  60. package/dist/packages/core/package.json +21 -0
  61. package/dist/packages/core/package.json.bak +21 -0
  62. package/dist/packages/core/src/extension-api/extension-manager.d.ts +35 -0
  63. package/dist/packages/core/src/extension-api/extension-manager.js +300 -0
  64. package/dist/packages/core/src/extension-api/extension-manager.js.map +1 -0
  65. package/dist/packages/core/src/extension-api/index.d.ts +2 -0
  66. package/dist/packages/core/src/extension-api/index.js +6 -0
  67. package/dist/packages/core/src/extension-api/index.js.map +1 -0
  68. package/dist/packages/core/src/extension-api/types.d.ts +34 -0
  69. package/dist/packages/core/src/extension-api/types.js +3 -0
  70. package/dist/packages/core/src/extension-api/types.js.map +1 -0
  71. package/dist/packages/core/src/index.d.ts +3 -0
  72. package/dist/packages/core/src/index.js +7 -0
  73. package/dist/packages/core/src/index.js.map +1 -0
  74. package/dist/packages/core/src/schemas/capability.schema.json +104 -0
  75. package/dist/packages/core/src/schemas/contract.schema.json +52 -0
  76. package/dist/packages/core/src/schemas/domain.schema.json +47 -0
  77. package/dist/packages/core/src/schemas/entity.schema.json +72 -0
  78. package/dist/packages/core/src/schemas/extension.schema.json +39 -0
  79. package/dist/packages/core/src/schemas/permission.schema.json +21 -0
  80. package/dist/packages/core/src/types/ast.d.ts +17 -0
  81. package/dist/packages/core/src/types/ast.js +3 -0
  82. package/dist/packages/core/src/types/ast.js.map +1 -0
  83. package/dist/packages/core/src/types/domain.d.ts +101 -0
  84. package/dist/packages/core/src/types/domain.js +8 -0
  85. package/dist/packages/core/src/types/domain.js.map +1 -0
  86. package/dist/packages/core/src/types/validation.d.ts +12 -0
  87. package/dist/packages/core/src/types/validation.js +16 -0
  88. package/dist/packages/core/src/types/validation.js.map +1 -0
  89. package/dist/packages/core/src/validator/ast-validator.d.ts +11 -0
  90. package/dist/packages/core/src/validator/ast-validator.js +388 -0
  91. package/dist/packages/core/src/validator/ast-validator.js.map +1 -0
  92. package/dist/packages/core/src/validator/index.d.ts +23 -0
  93. package/dist/packages/core/src/validator/index.js +143 -0
  94. package/dist/packages/core/src/validator/index.js.map +1 -0
  95. package/dist/packages/core/src/validator/serializer.d.ts +11 -0
  96. package/dist/packages/core/src/validator/serializer.js +99 -0
  97. package/dist/packages/core/src/validator/serializer.js.map +1 -0
  98. package/eslint.config.js +114 -0
  99. package/generate_prompts.js +19 -0
  100. package/jest.config.ts +5 -0
  101. package/jest.preset.js +3 -0
  102. package/nx.json +169 -0
  103. package/package.json +115 -0
  104. package/packages/angular-renderer/CHANGELOG.md +274 -0
  105. package/packages/angular-renderer/README.md +11 -0
  106. package/packages/angular-renderer/package.json +20 -0
  107. package/packages/angular-renderer/playwright-ct.config.ts +23 -0
  108. package/packages/angular-renderer/src/adapters/web/adapter.ts +145 -0
  109. package/packages/angular-renderer/src/adapters/web/experience-adapter.service.ts +21 -0
  110. package/packages/angular-renderer/src/components/primitives/button/button.component.html +9 -0
  111. package/packages/angular-renderer/src/components/primitives/button/button.component.pw.ts +19 -0
  112. package/packages/angular-renderer/src/components/primitives/button/button.component.scss +31 -0
  113. package/packages/angular-renderer/src/components/primitives/button/button.component.ts +67 -0
  114. package/packages/angular-renderer/src/components/primitives/primitives.a11y.pw.ts +33 -0
  115. package/packages/angular-renderer/src/components/primitives/text-input/text-input.component.html +10 -0
  116. package/packages/angular-renderer/src/components/primitives/text-input/text-input.component.pw.ts +29 -0
  117. package/packages/angular-renderer/src/components/primitives/text-input/text-input.component.scss +42 -0
  118. package/packages/angular-renderer/src/components/primitives/text-input/text-input.component.ts +84 -0
  119. package/packages/angular-renderer/src/components/primitives/vbox/vbox.component.html +2 -0
  120. package/packages/angular-renderer/src/components/primitives/vbox/vbox.component.pw.ts +19 -0
  121. package/packages/angular-renderer/src/components/primitives/vbox/vbox.component.scss +5 -0
  122. package/packages/angular-renderer/src/components/primitives/vbox/vbox.component.ts +74 -0
  123. package/packages/angular-renderer/src/index.ts +7 -0
  124. package/packages/angular-renderer/src/lib/renderer.component.ts +118 -0
  125. package/packages/angular-renderer/src/lib/renderer.tokens.ts +9 -0
  126. package/packages/angular-renderer/src/lib/test-utils.ts +26 -0
  127. package/packages/angular-renderer/src/lib/theme.provider.ts +83 -0
  128. package/packages/angular-renderer/src/test-setup.ts +8 -0
  129. package/packages/cli/CHANGELOG.md +92 -0
  130. package/packages/cli/README.md +11 -0
  131. package/packages/cli/package.json +16 -0
  132. package/packages/cli/src/commands/generate/entity.ts +20 -0
  133. package/packages/cli/src/commands/generate/index.ts +28 -0
  134. package/packages/cli/src/commands/init.ts +28 -0
  135. package/packages/cli/src/commands/new.ts +22 -0
  136. package/packages/cli/src/commands/validate.ts +47 -0
  137. package/packages/cli/src/index.ts +11 -0
  138. package/packages/cli/src/lib/cli.ts +3 -0
  139. package/packages/cli/src/lib/generator.ts +181 -0
  140. package/packages/cli/src/lib/scaffolding.ts +104 -0
  141. package/packages/cli/src/lib/validation.ts +218 -0
  142. package/packages/cli/src/main.ts +38 -0
  143. package/packages/cli/src/templates/entity.json +6 -0
  144. package/packages/cli/src/templates/extension.json +8 -0
  145. package/packages/cli/src/templates/index.ts +94 -0
  146. package/packages/cli/src/templates/origo.json +16 -0
  147. package/packages/cli/src/utils/errors.ts +80 -0
  148. package/packages/core/CHANGELOG.md +275 -0
  149. package/packages/core/README.md +11 -0
  150. package/packages/core/package-lock.json +1035 -0
  151. package/packages/core/package.json +21 -0
  152. package/packages/core/scripts/generate-types.ts +23 -0
  153. package/packages/core/src/extension-api/extension-manager.ts +378 -0
  154. package/packages/core/src/extension-api/index.ts +2 -0
  155. package/packages/core/src/extension-api/types.ts +37 -0
  156. package/packages/core/src/index.ts +3 -0
  157. package/packages/core/src/schemas/__fixtures__/target-page.json +222 -0
  158. package/packages/core/src/schemas/capability.schema.json +104 -0
  159. package/packages/core/src/schemas/contract.schema.json +52 -0
  160. package/packages/core/src/schemas/domain.schema.json +47 -0
  161. package/packages/core/src/schemas/entity.schema.json +72 -0
  162. package/packages/core/src/schemas/extension.schema.json +39 -0
  163. package/packages/core/src/schemas/permission.schema.json +21 -0
  164. package/packages/core/src/types/ast.ts +20 -0
  165. package/packages/core/src/types/domain.ts +105 -0
  166. package/packages/core/src/types/validation.ts +29 -0
  167. package/packages/core/src/validator/ast-validator.ts +406 -0
  168. package/packages/core/src/validator/index.ts +207 -0
  169. package/packages/core/src/validator/serializer.ts +120 -0
  170. package/packages/core/test/fixtures/target-page.json +87 -0
  171. package/packages/design-tokens/CHANGELOG.md +273 -0
  172. package/packages/design-tokens/README.md +11 -0
  173. package/packages/design-tokens/package.json +25 -0
  174. package/packages/design-tokens/src/build.ts +97 -0
  175. package/packages/design-tokens/src/compile.ts +60 -0
  176. package/packages/design-tokens/src/index.ts +7 -0
  177. package/packages/design-tokens/src/runtime/index.ts +2 -0
  178. package/packages/design-tokens/src/runtime/theme-fetcher.ts +90 -0
  179. package/packages/design-tokens/src/runtime/theme-injector.ts +149 -0
  180. package/packages/design-tokens/src/schemas/base-tokens.schema.json +127 -0
  181. package/packages/design-tokens/src/schemas/semantic-tokens.schema.json +52 -0
  182. package/packages/design-tokens/src/tokens/base.json +46 -0
  183. package/packages/design-tokens/src/tokens/semantic.json +33 -0
  184. package/packages/playground/CHANGELOG.md +12 -0
  185. package/packages/playground/package.json +10 -0
  186. package/packages/playground/public/favicon.ico +0 -0
  187. package/packages/playground/src/app/app.component.html +1 -0
  188. package/packages/playground/src/app/app.component.scss +4 -0
  189. package/packages/playground/src/app/app.component.ts +13 -0
  190. package/packages/playground/src/app/app.config.ts +9 -0
  191. package/packages/playground/src/editor/badl-editor.component.html +1 -0
  192. package/packages/playground/src/editor/badl-editor.component.scss +10 -0
  193. package/packages/playground/src/editor/badl-editor.component.ts +57 -0
  194. package/packages/playground/src/editor/editor.worker.ts +1 -0
  195. package/packages/playground/src/editor/json.worker.ts +1 -0
  196. package/packages/playground/src/editor/monaco-environment.ts +11 -0
  197. package/packages/playground/src/editor/schema-registry.ts +30 -0
  198. package/packages/playground/src/index.html +13 -0
  199. package/packages/playground/src/main.ts +5 -0
  200. package/packages/playground/src/schemas/capability.schema.json +104 -0
  201. package/packages/playground/src/schemas/contract.schema.json +52 -0
  202. package/packages/playground/src/schemas/domain.schema.json +47 -0
  203. package/packages/playground/src/schemas/entity.schema.json +72 -0
  204. package/packages/playground/src/schemas/extension.schema.json +39 -0
  205. package/packages/playground/src/schemas/permission.schema.json +21 -0
  206. package/packages/playground/src/styles.scss +8 -0
  207. package/packages/playground/src/test-setup.ts +146 -0
  208. package/packages/playground/vite.config.ts +12 -0
  209. package/packages/playground/vitest.config.mjs +24 -0
@@ -0,0 +1,406 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { CanonicalAST } from '../types/ast';
3
+ import { ValidationError } from '../types/validation';
4
+ import { Contract, Capability } from '../types/domain';
5
+ import * as semver from 'semver';
6
+
7
+ export const MAX_AST_DEPTH = 250;
8
+
9
+ /**
10
+ * Performs deep semantic validation on the Canonical AST before downstream code generation.
11
+ * Specifically checks for cyclic entity relationships and missing references.
12
+ *
13
+ * @param ast The Canonical AST to validate
14
+ * @returns {ValidationError[]} Array of validation errors, or empty array if valid.
15
+ */
16
+ export function validateAST(
17
+ ast: CanonicalAST,
18
+ localManifest?: Record<string, string>
19
+ ): ValidationError[] {
20
+ const errors: ValidationError[] = [];
21
+
22
+ if (!ast || !Array.isArray(ast.domains)) {
23
+ errors.push({ type: 'INVALID_FORMAT', message: 'Invalid AST: missing domains array' });
24
+ return errors;
25
+ }
26
+ const entityDomainMap = new Map<string, string>();
27
+ const extensionIds = new Set<string>();
28
+ const capabilityIds = new Set<string>();
29
+ const contractMap = new Map<string, Contract>();
30
+ const entityCapabilities = new Map<string, Capability[]>();
31
+
32
+ // First pass: Collect all entities, capabilities, and contracts to ensure unique IDs and for quick lookup
33
+ for (let dIndex = 0; dIndex < ast.domains.length; dIndex++) {
34
+ const domain = ast.domains[dIndex];
35
+ if (!domain) continue;
36
+ const entities = Array.isArray(domain.entities) ? domain.entities : [];
37
+ for (let eIndex = 0; eIndex < entities.length; eIndex++) {
38
+ const entity = entities[eIndex];
39
+ if (!entity || !entity.id) continue;
40
+ if (entityDomainMap.has(entity.id)) {
41
+ errors.push({
42
+ type: 'DUPLICATE_ID',
43
+ message: `Duplicate entity ID found: ${entity.id}`,
44
+ path: `/domains/${dIndex}/entities/${eIndex}/id`,
45
+ });
46
+ } else {
47
+ entityDomainMap.set(entity.id, domain.id);
48
+ }
49
+ }
50
+
51
+ const capabilities = Array.isArray(domain.capabilities) ? domain.capabilities : [];
52
+ for (let cIndex = 0; cIndex < capabilities.length; cIndex++) {
53
+ const capability = capabilities[cIndex];
54
+ if (!capability || !capability.id) {
55
+ errors.push({
56
+ type: 'INVALID_FORMAT',
57
+ message: `Capability missing id in domain ${domain.id}`,
58
+ path: `/domains/${dIndex}/capabilities/${cIndex}`,
59
+ });
60
+ continue;
61
+ }
62
+ if (capabilityIds.has(capability.id)) {
63
+ errors.push({
64
+ type: 'DUPLICATE_ID',
65
+ message: `Duplicate capability ID found: ${capability.id}`,
66
+ path: `/domains/${dIndex}/capabilities/${cIndex}/id`,
67
+ });
68
+ continue;
69
+ } else {
70
+ capabilityIds.add(capability.id);
71
+ }
72
+
73
+ if (capability.entityId) {
74
+ const existingCaps = entityCapabilities.get(capability.entityId) || [];
75
+ existingCaps.push(capability);
76
+ entityCapabilities.set(capability.entityId, existingCaps);
77
+ }
78
+ }
79
+
80
+ const contracts = Array.isArray(domain.contracts) ? domain.contracts : [];
81
+ for (let cIdx = 0; cIdx < contracts.length; cIdx++) {
82
+ const contract = contracts[cIdx];
83
+ if (!contract || !contract.id) {
84
+ errors.push({
85
+ type: 'INVALID_FORMAT',
86
+ message: 'Contract missing id',
87
+ path: `/domains/${dIndex}/contracts/${cIdx}`,
88
+ });
89
+ continue;
90
+ }
91
+ if (contractMap.has(contract.id)) {
92
+ errors.push({
93
+ type: 'DUPLICATE_ID',
94
+ message: `Duplicate contract ID found: ${contract.id}`,
95
+ path: `/domains/${dIndex}/contracts/${cIdx}/id`,
96
+ });
97
+ } else {
98
+ contractMap.set(contract.id, contract);
99
+ }
100
+ }
101
+
102
+ const extensions = Array.isArray(domain.extensions) ? domain.extensions : [];
103
+ for (let eIdx = 0; eIdx < extensions.length; eIdx++) {
104
+ const ext = extensions[eIdx];
105
+ if (!ext || !ext.id) {
106
+ errors.push({
107
+ type: 'INVALID_FORMAT',
108
+ message: 'Extension missing id',
109
+ path: `/domains/${dIndex}/extensions/${eIdx}`,
110
+ });
111
+ continue;
112
+ }
113
+ if (
114
+ contractMap.has(ext.id) ||
115
+ capabilityIds.has(ext.id) ||
116
+ entityDomainMap.has(ext.id) ||
117
+ extensionIds.has(ext.id)
118
+ ) {
119
+ errors.push({
120
+ type: 'DUPLICATE_ID',
121
+ message: `Duplicate ID found across extensions and other types: ${ext.id}`,
122
+ path: `/domains/${dIndex}/extensions/${eIdx}/id`,
123
+ });
124
+ } else {
125
+ extensionIds.add(ext.id);
126
+ }
127
+ }
128
+ }
129
+
130
+ // Second pass: Validate field references and detect cycles
131
+ const adjList = new Map<string, string[]>();
132
+
133
+ for (let dIndex = 0; dIndex < ast.domains.length; dIndex++) {
134
+ const domain = ast.domains[dIndex];
135
+ if (!domain) continue;
136
+ const entities = Array.isArray(domain.entities) ? domain.entities : [];
137
+ for (let eIndex = 0; eIndex < entities.length; eIndex++) {
138
+ const entity = entities[eIndex];
139
+ if (!entity || !entity.id) continue;
140
+ const dependencies: string[] = [];
141
+ const processFields = (fieldList: any[], currentPath: string, depth = 1) => {
142
+ if (depth > MAX_AST_DEPTH) {
143
+ errors.push({
144
+ type: 'MAX_DEPTH_EXCEEDED',
145
+ message: `Maximum AST depth exceeded (${MAX_AST_DEPTH}) in entity fields for ${entity.id}`,
146
+ path: currentPath,
147
+ });
148
+ return;
149
+ }
150
+ for (let fIndex = 0; fIndex < fieldList.length; fIndex++) {
151
+ const field = fieldList[fIndex];
152
+ if (!field || typeof field !== 'object') continue;
153
+ const fieldPath = `${currentPath}/${fIndex}`;
154
+ if (field.references) {
155
+ if (!entityDomainMap.has(field.references)) {
156
+ errors.push({
157
+ type: 'MISSING_REFERENCE',
158
+ message: `Invalid consumption rule: Entity "${field.references}" referenced by field "${field.id}" does not exist`,
159
+ path: `${fieldPath}/references`,
160
+ });
161
+ } else {
162
+ dependencies.push(field.references);
163
+ }
164
+ }
165
+ if (Array.isArray(field.fields)) {
166
+ processFields(field.fields, `${fieldPath}/fields`, depth + 1);
167
+ }
168
+ }
169
+ };
170
+
171
+ const fields = Array.isArray(entity.fields) ? entity.fields : [];
172
+ processFields(fields, `/domains/${dIndex}/entities/${eIndex}/fields`);
173
+ const existingDeps = adjList.get(entity.id) || [];
174
+ adjList.set(entity.id, [...existingDeps, ...dependencies]);
175
+
176
+ if (Array.isArray(entity.implements)) {
177
+ for (const contractId of entity.implements) {
178
+ const contract = contractMap.get(contractId);
179
+ if (!contract) {
180
+ errors.push({
181
+ type: 'MISSING_REFERENCE',
182
+ message: `Entity "${entity.id}" implements missing contract "${contractId}"`,
183
+ path: `domains[${dIndex}].entities[${eIndex}].implements`,
184
+ });
185
+ continue;
186
+ }
187
+
188
+ if (Array.isArray(contract.requiredFields)) {
189
+ const flattenFields = (fieldList: any[]): any[] => {
190
+ let res: any[] = [];
191
+ for (const f of fieldList) {
192
+ if (!f) continue;
193
+ res.push(f);
194
+ if (Array.isArray(f.fields)) {
195
+ res = res.concat(flattenFields(f.fields));
196
+ }
197
+ }
198
+ return res;
199
+ };
200
+ const allEntityFields = flattenFields(entity.fields || []);
201
+ for (const reqField of contract.requiredFields) {
202
+ if (!reqField || typeof reqField !== 'object' || !reqField.name) continue;
203
+ const entityField = allEntityFields.find((f: any) => f.name === reqField.name);
204
+ if (!entityField) {
205
+ errors.push({
206
+ type: 'CONTRACT_BREACH',
207
+ message: `Entity "${entity.id}" missing required field "${reqField.name}" for contract "${contract.id}"`,
208
+ path: `/domains/${dIndex}/entities/${eIndex}/implements`,
209
+ });
210
+ } else if (entityField.type !== reqField.type) {
211
+ errors.push({
212
+ type: 'CONTRACT_BREACH',
213
+ message: `Entity "${entity.id}" field "${reqField.name}" has type "${entityField.type}" but contract "${contract.id}" requires "${reqField.type}"`,
214
+ path: `/domains/${dIndex}/entities/${eIndex}/fields`,
215
+ });
216
+ }
217
+ }
218
+ }
219
+
220
+ if (Array.isArray(contract.requiredCapabilities)) {
221
+ const caps = entityCapabilities.get(entity.id) || [];
222
+ for (const reqCap of contract.requiredCapabilities) {
223
+ if (!reqCap || typeof reqCap !== 'object' || !reqCap.name || !reqCap.type) continue;
224
+ const hasCap = caps.some(
225
+ (c: any) => c.name === reqCap.name && c.type === reqCap.type
226
+ );
227
+ if (!hasCap) {
228
+ errors.push({
229
+ type: 'CONTRACT_BREACH',
230
+ message: `Entity "${entity.id}" missing required capability "${reqCap.name}" (${reqCap.type}) for contract "${contract.id}"`,
231
+ path: `/domains/${dIndex}/entities/${eIndex}/implements`,
232
+ });
233
+ }
234
+ }
235
+ }
236
+ }
237
+ }
238
+ }
239
+
240
+ const capabilities = Array.isArray(domain.capabilities) ? domain.capabilities : [];
241
+ for (let cIndex = 0; cIndex < capabilities.length; cIndex++) {
242
+ const capability = capabilities[cIndex];
243
+ if (!capability || !capability.id) continue;
244
+
245
+ const capPath = `/domains/${dIndex}/capabilities/${cIndex}`;
246
+ if (!capability.entityId) {
247
+ errors.push({
248
+ type: 'MISSING_REFERENCE',
249
+ message: `Capability missing entityId: ${capability.id}`,
250
+ path: `${capPath}/entityId`,
251
+ });
252
+ continue;
253
+ }
254
+
255
+ const targetDomainId = entityDomainMap.get(capability.entityId);
256
+ if (!targetDomainId) {
257
+ errors.push({
258
+ type: 'MISSING_REFERENCE',
259
+ message: `Invalid capability reference: Entity "${capability.entityId}" referenced by capability "${capability.id}" does not exist`,
260
+ path: `${capPath}/entityId`,
261
+ });
262
+ }
263
+
264
+ if (
265
+ !capability.permissions ||
266
+ !Array.isArray(capability.permissions) ||
267
+ capability.permissions.length === 0
268
+ ) {
269
+ errors.push({
270
+ type: 'UNSECURED_CAPABILITY',
271
+ message: `Capability "${capability.id}" is unsecured. Fail-closed policy requires at least one permission.`,
272
+ path: `${capPath}/permissions`,
273
+ });
274
+ } else {
275
+ for (let pIndex = 0; pIndex < capability.permissions.length; pIndex++) {
276
+ const perm = capability.permissions[pIndex];
277
+ if (
278
+ !perm ||
279
+ typeof perm !== 'object' ||
280
+ !perm.role ||
281
+ typeof perm.role !== 'string' ||
282
+ perm.role.trim() === ''
283
+ ) {
284
+ errors.push({
285
+ type: 'INVALID_PERMISSION',
286
+ message: `Capability "${capability.id}" has an invalid permission definition. Role is required.`,
287
+ path: `${capPath}/permissions/${pIndex}`,
288
+ });
289
+ } else {
290
+ perm.role = perm.role.trim();
291
+ if (perm.access === undefined) {
292
+ perm.access = 'grant';
293
+ }
294
+ if (perm.access !== 'grant' && perm.access !== 'deny') {
295
+ errors.push({
296
+ type: 'INVALID_PERMISSION',
297
+ message: `Capability "${capability.id}" has an invalid access definition. Must be "grant" or "deny".`,
298
+ path: `${capPath}/permissions/${pIndex}/access`,
299
+ });
300
+ }
301
+ }
302
+ }
303
+ }
304
+ }
305
+
306
+ const extensions = Array.isArray(domain.extensions) ? domain.extensions : [];
307
+ for (let eIndex = 0; eIndex < extensions.length; eIndex++) {
308
+ const ext = extensions[eIndex];
309
+ if (!ext || !ext.id) continue;
310
+
311
+ const extPath = `/domains/${dIndex}/extensions/${eIndex}`;
312
+ if (!Array.isArray(ext.implements) || ext.implements.length === 0) {
313
+ errors.push({
314
+ type: 'MISSING_REFERENCE',
315
+ message: `Extension missing implements contract reference: ${ext.id}`,
316
+ path: `${extPath}/implements`,
317
+ });
318
+ } else {
319
+ for (const imp of ext.implements) {
320
+ if (!contractMap.has(imp)) {
321
+ errors.push({
322
+ type: 'MISSING_REFERENCE',
323
+ message: `Invalid extension reference: Contract "${imp}" referenced by extension "${ext.id}" does not exist`,
324
+ path: `${extPath}/implements`,
325
+ });
326
+ }
327
+ }
328
+ }
329
+
330
+ if (localManifest && ext.name && ext.plugin_version_range) {
331
+ const localVersion = localManifest[ext.name];
332
+ if (!localVersion) {
333
+ errors.push({
334
+ type: 'MISSING_MANIFEST_VERSION',
335
+ message: `Local manifest is missing version for extension "${ext.name}"`,
336
+ path: `${extPath}/name`,
337
+ });
338
+ } else if (!semver.valid(localVersion) || !semver.validRange(ext.plugin_version_range)) {
339
+ errors.push({
340
+ type: 'VERSION_MISMATCH',
341
+ message: `Invalid semver format for extension "${ext.name}"`,
342
+ path: `${extPath}/plugin_version_range`,
343
+ });
344
+ } else if (!semver.satisfies(localVersion, ext.plugin_version_range)) {
345
+ errors.push({
346
+ type: 'VERSION_MISMATCH',
347
+ message: `Extension "${ext.name}" requires plugin version "${ext.plugin_version_range}" but local manifest provides "${localVersion}"`,
348
+ path: `${extPath}/plugin_version_range`,
349
+ });
350
+ }
351
+ }
352
+ }
353
+ }
354
+
355
+ // Detect cycles using DFS
356
+ const visited = new Set<string>();
357
+ const visiting = new Set<string>();
358
+
359
+ function dfs(nodeId: string, path: string[], depth: number) {
360
+ if (depth > MAX_AST_DEPTH) {
361
+ errors.push({
362
+ type: 'MAX_DEPTH_EXCEEDED',
363
+ message: `Maximum AST depth exceeded (${MAX_AST_DEPTH}) at entity ${nodeId}`,
364
+ path: [...path, nodeId].join(' -> '),
365
+ });
366
+ return;
367
+ }
368
+
369
+ if (visiting.has(nodeId)) {
370
+ // Cycle detected: isolate the loop part
371
+ const cycleStartIndex = path.indexOf(nodeId);
372
+ const loop = cycleStartIndex >= 0 ? path.slice(cycleStartIndex) : path;
373
+ const cyclePath = [...loop, nodeId].join(' -> ');
374
+ errors.push({
375
+ type: 'CIRCULAR_REFERENCE',
376
+ message: `Circular dependency detected: ${cyclePath}`,
377
+ path: cyclePath,
378
+ });
379
+ return;
380
+ }
381
+
382
+ if (visited.has(nodeId)) {
383
+ return;
384
+ }
385
+
386
+ visiting.add(nodeId);
387
+ path.push(nodeId);
388
+
389
+ const deps = adjList.get(nodeId) || [];
390
+ for (const dep of deps) {
391
+ dfs(dep, path, depth + 1);
392
+ }
393
+
394
+ path.pop();
395
+ visiting.delete(nodeId);
396
+ visited.add(nodeId);
397
+ }
398
+
399
+ for (const entityId of entityDomainMap.keys()) {
400
+ if (!visited.has(entityId)) {
401
+ dfs(entityId, [], 1);
402
+ }
403
+ }
404
+
405
+ return errors;
406
+ }
@@ -0,0 +1,207 @@
1
+ import Ajv, { ErrorObject } from 'ajv/dist/2020';
2
+ import addFormats from 'ajv-formats';
3
+ import addErrors from 'ajv-errors';
4
+ import { parse as parseJSONWithSourceMap } from 'json-source-map';
5
+ import * as domainSchema from '../schemas/domain.schema.json';
6
+ import * as entitySchema from '../schemas/entity.schema.json';
7
+ import * as capabilitySchema from '../schemas/capability.schema.json';
8
+ import * as contractSchema from '../schemas/contract.schema.json';
9
+ import * as permissionSchema from '../schemas/permission.schema.json';
10
+ import * as extensionSchema from '../schemas/extension.schema.json';
11
+
12
+ export interface ValidatorOptions {
13
+ maxDepth?: number;
14
+ }
15
+
16
+ export interface EnhancedErrorObject extends ErrorObject {
17
+ code?: string;
18
+ context?: {
19
+ line?: number;
20
+ column?: number;
21
+ [key: string]: unknown;
22
+ };
23
+ }
24
+
25
+ export class BADLValidator {
26
+ private ajv: Ajv;
27
+ public errors: EnhancedErrorObject[] | null | undefined = null;
28
+
29
+ constructor() {
30
+ this.ajv = new Ajv({
31
+ allErrors: true,
32
+ strict: true,
33
+ });
34
+
35
+ addFormats(this.ajv);
36
+ addErrors(this.ajv);
37
+
38
+ const resolvedEntitySchema =
39
+ (entitySchema as Record<string, unknown>)['default'] ?? entitySchema;
40
+ const resolvedDomainSchema =
41
+ (domainSchema as Record<string, unknown>)['default'] ?? domainSchema;
42
+ const resolvedCapabilitySchema =
43
+ (capabilitySchema as Record<string, unknown>)['default'] ?? capabilitySchema;
44
+ const resolvedPermissionSchema =
45
+ (permissionSchema as Record<string, unknown>)['default'] ?? permissionSchema;
46
+
47
+ const resolvedContractSchema =
48
+ (contractSchema as Record<string, unknown>)['default'] ?? contractSchema;
49
+
50
+ const resolvedExtensionSchema =
51
+ (extensionSchema as Record<string, unknown>)['default'] ?? extensionSchema;
52
+
53
+ this.ajv.addSchema(
54
+ resolvedCapabilitySchema,
55
+ 'https://origo.design/schemas/v1/capability.schema.json'
56
+ );
57
+ this.ajv.addSchema(
58
+ resolvedContractSchema,
59
+ 'https://origo.design/schemas/v1/contract.schema.json'
60
+ );
61
+ this.ajv.addSchema(
62
+ resolvedPermissionSchema,
63
+ 'https://origo.design/schemas/v1/permission.schema.json'
64
+ );
65
+ this.ajv.addSchema(
66
+ resolvedExtensionSchema,
67
+ 'https://origo.design/schemas/v1/extension.schema.json'
68
+ );
69
+ this.ajv.addSchema(resolvedEntitySchema, 'https://origo.design/schemas/v1/entity.schema.json');
70
+ this.ajv.addSchema(resolvedDomainSchema, 'https://origo.design/schemas/v1/domain.schema.json');
71
+ }
72
+
73
+ private checkCircularDependency(
74
+ obj: unknown,
75
+ currentDepth: number,
76
+ maxDepth: number,
77
+ visited: Set<unknown>
78
+ ) {
79
+ if (currentDepth > maxDepth) {
80
+ throw new Error('Maximum depth exceeded. Possible circular dependency detected.');
81
+ }
82
+
83
+ if (obj && typeof obj === 'object') {
84
+ if (visited.has(obj)) {
85
+ throw new Error('Maximum depth exceeded. Possible circular dependency detected.');
86
+ }
87
+ visited.add(obj);
88
+
89
+ for (const key in obj) {
90
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
91
+ this.checkCircularDependency(
92
+ (obj as Record<string, unknown>)[key],
93
+ currentDepth + 1,
94
+ maxDepth,
95
+ visited
96
+ );
97
+ }
98
+ }
99
+ visited.delete(obj);
100
+ }
101
+ }
102
+
103
+ private runValidation(schemaUrl: string, data: unknown, options: ValidatorOptions = {}): boolean {
104
+ this.errors = null;
105
+ let parsedData = data;
106
+ let sourceMapPointers: Record<string, unknown> | undefined;
107
+
108
+ if (typeof data === 'string') {
109
+ try {
110
+ const parsed = parseJSONWithSourceMap(data);
111
+ parsedData = parsed.data;
112
+ sourceMapPointers = parsed.pointers;
113
+ } catch (err: unknown) {
114
+ let line: number | undefined;
115
+ let column: number | undefined;
116
+
117
+ if (err && typeof err === 'object') {
118
+ const errMsg = (err as Error).message || '';
119
+ const match = /line (\d+) column (\d+)/.exec(errMsg);
120
+ if (match) {
121
+ line = parseInt(match[1], 10);
122
+ column = parseInt(match[2], 10);
123
+ }
124
+ }
125
+
126
+ this.errors = [
127
+ {
128
+ keyword: 'parse',
129
+ message: (err as Error)?.message || 'Invalid JSON string',
130
+ instancePath: '',
131
+ schemaPath: '',
132
+ params: {},
133
+ code: 'parse',
134
+ context: { line, column },
135
+ },
136
+ ];
137
+ return false;
138
+ }
139
+ }
140
+
141
+ if (typeof options.maxDepth === 'number' && options.maxDepth < 0) {
142
+ throw new Error('maxDepth must be >= 0');
143
+ }
144
+ const maxDepth = options.maxDepth ?? 100;
145
+ this.checkCircularDependency(parsedData, 0, maxDepth, new Set());
146
+
147
+ const validate = this.ajv.getSchema(schemaUrl);
148
+ if (!validate) {
149
+ throw new Error(`Schema not found: ${schemaUrl}`);
150
+ }
151
+
152
+ const isValid = validate(parsedData);
153
+
154
+ if (!isValid && validate.errors) {
155
+ this.errors = validate.errors.map(err => {
156
+ const newErr: EnhancedErrorObject = { ...err, code: err.keyword };
157
+ if (sourceMapPointers) {
158
+ const decodedPath = (err.instancePath ?? '').replace(/~1/g, '/').replace(/~0/g, '~');
159
+
160
+ let lookupPath = decodedPath;
161
+ if (
162
+ err.keyword === 'additionalProperties' &&
163
+ (err.params as Record<string, unknown>)['additionalProperty']
164
+ ) {
165
+ const additionalProperty = (err.params as Record<string, unknown>)[
166
+ 'additionalProperty'
167
+ ] as string;
168
+ const additionalPath = `${decodedPath === '' ? '' : decodedPath}/${additionalProperty.replace(/~/g, '~0').replace(/\//g, '~1')}`;
169
+ if (sourceMapPointers[additionalPath]) {
170
+ lookupPath = additionalPath;
171
+ }
172
+ }
173
+
174
+ if (sourceMapPointers[lookupPath] !== undefined) {
175
+ const pointer = sourceMapPointers[lookupPath] as {
176
+ key?: { line: number; column: number };
177
+ value?: { line: number; column: number };
178
+ };
179
+ const loc = pointer.key || pointer.value;
180
+ if (loc != null) {
181
+ newErr.context = {
182
+ line: loc.line + 1,
183
+ column: loc.column + 1,
184
+ };
185
+ }
186
+ }
187
+ }
188
+ return newErr;
189
+ });
190
+ } else {
191
+ this.errors = null;
192
+ }
193
+
194
+ return isValid as boolean;
195
+ }
196
+
197
+ validateDomain(data: unknown, options: ValidatorOptions = {}): boolean {
198
+ return this.runValidation('https://origo.design/schemas/v1/domain.schema.json', data, options);
199
+ }
200
+
201
+ validateEntity(data: unknown, options: ValidatorOptions = {}): boolean {
202
+ return this.runValidation('https://origo.design/schemas/v1/entity.schema.json', data, options);
203
+ }
204
+ }
205
+
206
+ export * from './serializer';
207
+ export * from './ast-validator';