@juangcarmona/lighthouse 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/profiles/catalog.json +1019 -17
- package/assets/profiles/mobile-expo/README.md +19 -0
- package/assets/profiles/mobile-expo/SKILL.md +14 -0
- package/assets/profiles/mobile-expo/adoption.md +11 -0
- package/assets/profiles/mobile-expo/guardrails.md +14 -0
- package/assets/profiles/mobile-expo/testing.md +13 -0
- package/assets/profiles/mobile-maui/README.md +19 -0
- package/assets/profiles/mobile-maui/SKILL.md +14 -0
- package/assets/profiles/mobile-maui/adoption.md +11 -0
- package/assets/profiles/mobile-maui/guardrails.md +13 -0
- package/assets/profiles/mobile-maui/testing.md +13 -0
- package/assets/profiles/pwa-capability/README.md +19 -0
- package/assets/profiles/pwa-capability/SKILL.md +14 -0
- package/assets/profiles/pwa-capability/adoption.md +9 -0
- package/assets/profiles/pwa-capability/guardrails.md +13 -0
- package/assets/profiles/pwa-capability/offline.md +11 -0
- package/assets/profiles/pwa-capability/testing.md +15 -0
- package/assets/profiles/react-web-platform/README.md +24 -0
- package/assets/profiles/react-web-platform/SKILL.md +18 -0
- package/assets/profiles/react-web-platform/adoption.md +17 -0
- package/assets/profiles/react-web-platform/guardrails.md +17 -0
- package/assets/profiles/react-web-platform/structure.md +36 -0
- package/assets/profiles/react-web-platform/testing.md +15 -0
- package/assets/profiles/web-bff/README.md +22 -0
- package/assets/profiles/web-bff/SKILL.md +16 -0
- package/assets/profiles/web-bff/adoption.md +9 -0
- package/assets/profiles/web-bff/security.md +16 -0
- package/assets/profiles/web-bff/testing.md +14 -0
- package/assets/schemas/profile-catalog.schema.json +286 -20
- package/package.json +1 -1
- package/src/lighthouse.mjs +46 -1
- package/src/profiles.mjs +78 -5
|
@@ -4,59 +4,325 @@
|
|
|
4
4
|
"title": "Lighthouse profile catalog",
|
|
5
5
|
"type": "object",
|
|
6
6
|
"additionalProperties": false,
|
|
7
|
-
"required": [
|
|
7
|
+
"required": [
|
|
8
|
+
"$schema",
|
|
9
|
+
"schemaVersion",
|
|
10
|
+
"profiles"
|
|
11
|
+
],
|
|
8
12
|
"properties": {
|
|
9
|
-
"$schema": {
|
|
10
|
-
|
|
13
|
+
"$schema": {
|
|
14
|
+
"type": "string"
|
|
15
|
+
},
|
|
16
|
+
"schemaVersion": {
|
|
17
|
+
"const": 1
|
|
18
|
+
},
|
|
11
19
|
"profiles": {
|
|
12
20
|
"type": "array",
|
|
13
|
-
"items": {
|
|
21
|
+
"items": {
|
|
22
|
+
"$ref": "#/$defs/profile"
|
|
23
|
+
}
|
|
14
24
|
}
|
|
15
25
|
},
|
|
16
26
|
"$defs": {
|
|
17
27
|
"profile": {
|
|
18
28
|
"type": "object",
|
|
19
29
|
"additionalProperties": false,
|
|
20
|
-
"required": [
|
|
30
|
+
"required": [
|
|
31
|
+
"id",
|
|
32
|
+
"owner",
|
|
33
|
+
"purpose",
|
|
34
|
+
"appliesTo",
|
|
35
|
+
"surfaces",
|
|
36
|
+
"prerequisites",
|
|
37
|
+
"dependencies",
|
|
38
|
+
"incompatibleWith",
|
|
39
|
+
"libraries",
|
|
40
|
+
"structure",
|
|
41
|
+
"commands",
|
|
42
|
+
"guardrails",
|
|
43
|
+
"failureConsequence",
|
|
44
|
+
"repairHints",
|
|
45
|
+
"exceptions",
|
|
46
|
+
"combinations",
|
|
47
|
+
"consumerFixture",
|
|
48
|
+
"upgradeStrategy",
|
|
49
|
+
"verification"
|
|
50
|
+
],
|
|
21
51
|
"properties": {
|
|
22
|
-
"id": {
|
|
23
|
-
|
|
24
|
-
|
|
52
|
+
"id": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
|
|
55
|
+
},
|
|
56
|
+
"owner": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"minLength": 1
|
|
59
|
+
},
|
|
60
|
+
"purpose": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"minLength": 1
|
|
63
|
+
},
|
|
25
64
|
"appliesTo": {
|
|
26
65
|
"type": "object",
|
|
27
66
|
"additionalProperties": false,
|
|
28
|
-
"required": [
|
|
67
|
+
"required": [
|
|
68
|
+
"shapes",
|
|
69
|
+
"stacks"
|
|
70
|
+
],
|
|
29
71
|
"properties": {
|
|
30
|
-
"shapes": {
|
|
31
|
-
|
|
72
|
+
"shapes": {
|
|
73
|
+
"type": "array",
|
|
74
|
+
"items": {
|
|
75
|
+
"type": "string"
|
|
76
|
+
},
|
|
77
|
+
"minItems": 1,
|
|
78
|
+
"uniqueItems": true
|
|
79
|
+
},
|
|
80
|
+
"stacks": {
|
|
81
|
+
"type": "array",
|
|
82
|
+
"items": {
|
|
83
|
+
"type": "string"
|
|
84
|
+
},
|
|
85
|
+
"uniqueItems": true
|
|
86
|
+
}
|
|
32
87
|
}
|
|
33
88
|
},
|
|
34
|
-
"surfaces": {
|
|
89
|
+
"surfaces": {
|
|
90
|
+
"type": "array",
|
|
91
|
+
"items": {
|
|
92
|
+
"type": "string",
|
|
93
|
+
"minLength": 1
|
|
94
|
+
},
|
|
95
|
+
"minItems": 1,
|
|
96
|
+
"uniqueItems": true
|
|
97
|
+
},
|
|
35
98
|
"verification": {
|
|
36
99
|
"type": "object",
|
|
37
100
|
"additionalProperties": false,
|
|
38
|
-
"required": [
|
|
101
|
+
"required": [
|
|
102
|
+
"layers"
|
|
103
|
+
],
|
|
39
104
|
"properties": {
|
|
40
105
|
"layers": {
|
|
41
106
|
"type": "array",
|
|
42
|
-
"items": {
|
|
107
|
+
"items": {
|
|
108
|
+
"$ref": "#/$defs/layer"
|
|
109
|
+
},
|
|
43
110
|
"minItems": 1,
|
|
44
111
|
"uniqueItems": true
|
|
45
112
|
}
|
|
46
113
|
}
|
|
114
|
+
},
|
|
115
|
+
"prerequisites": {
|
|
116
|
+
"type": "array",
|
|
117
|
+
"items": {
|
|
118
|
+
"type": "string",
|
|
119
|
+
"minLength": 1
|
|
120
|
+
},
|
|
121
|
+
"uniqueItems": true
|
|
122
|
+
},
|
|
123
|
+
"dependencies": {
|
|
124
|
+
"type": "array",
|
|
125
|
+
"items": {
|
|
126
|
+
"$ref": "#/$defs/profileId"
|
|
127
|
+
},
|
|
128
|
+
"uniqueItems": true
|
|
129
|
+
},
|
|
130
|
+
"incompatibleWith": {
|
|
131
|
+
"type": "array",
|
|
132
|
+
"items": {
|
|
133
|
+
"$ref": "#/$defs/profileId"
|
|
134
|
+
},
|
|
135
|
+
"uniqueItems": true
|
|
136
|
+
},
|
|
137
|
+
"libraries": {
|
|
138
|
+
"type": "array",
|
|
139
|
+
"items": {
|
|
140
|
+
"type": "string",
|
|
141
|
+
"minLength": 1
|
|
142
|
+
},
|
|
143
|
+
"uniqueItems": true
|
|
144
|
+
},
|
|
145
|
+
"structure": {
|
|
146
|
+
"type": "array",
|
|
147
|
+
"items": {
|
|
148
|
+
"type": "string",
|
|
149
|
+
"minLength": 1
|
|
150
|
+
},
|
|
151
|
+
"uniqueItems": true
|
|
152
|
+
},
|
|
153
|
+
"commands": {
|
|
154
|
+
"type": "array",
|
|
155
|
+
"items": {
|
|
156
|
+
"type": "string",
|
|
157
|
+
"minLength": 1
|
|
158
|
+
},
|
|
159
|
+
"uniqueItems": true
|
|
160
|
+
},
|
|
161
|
+
"guardrails": {
|
|
162
|
+
"type": "array",
|
|
163
|
+
"items": {
|
|
164
|
+
"$ref": "#/$defs/guardrail"
|
|
165
|
+
},
|
|
166
|
+
"uniqueItems": true
|
|
167
|
+
},
|
|
168
|
+
"failureConsequence": {
|
|
169
|
+
"type": "string",
|
|
170
|
+
"minLength": 1
|
|
171
|
+
},
|
|
172
|
+
"repairHints": {
|
|
173
|
+
"type": "array",
|
|
174
|
+
"items": {
|
|
175
|
+
"type": "string",
|
|
176
|
+
"minLength": 1
|
|
177
|
+
},
|
|
178
|
+
"minItems": 1
|
|
179
|
+
},
|
|
180
|
+
"exceptions": {
|
|
181
|
+
"type": "array",
|
|
182
|
+
"items": {
|
|
183
|
+
"type": "string",
|
|
184
|
+
"minLength": 1
|
|
185
|
+
},
|
|
186
|
+
"minItems": 1
|
|
187
|
+
},
|
|
188
|
+
"combinations": {
|
|
189
|
+
"type": "array",
|
|
190
|
+
"items": {
|
|
191
|
+
"$ref": "#/$defs/combination"
|
|
192
|
+
},
|
|
193
|
+
"uniqueItems": true
|
|
194
|
+
},
|
|
195
|
+
"consumerFixture": {
|
|
196
|
+
"type": "string",
|
|
197
|
+
"minLength": 1
|
|
198
|
+
},
|
|
199
|
+
"upgradeStrategy": {
|
|
200
|
+
"type": "string",
|
|
201
|
+
"minLength": 1
|
|
47
202
|
}
|
|
48
203
|
}
|
|
49
204
|
},
|
|
50
205
|
"layer": {
|
|
51
206
|
"type": "object",
|
|
52
207
|
"additionalProperties": false,
|
|
53
|
-
"required": [
|
|
208
|
+
"required": [
|
|
209
|
+
"id",
|
|
210
|
+
"level",
|
|
211
|
+
"kind",
|
|
212
|
+
"verification"
|
|
213
|
+
],
|
|
54
214
|
"properties": {
|
|
55
|
-
"id": {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
215
|
+
"id": {
|
|
216
|
+
"enum": [
|
|
217
|
+
"static",
|
|
218
|
+
"unit",
|
|
219
|
+
"architecture",
|
|
220
|
+
"integration",
|
|
221
|
+
"functional",
|
|
222
|
+
"browser-e2e",
|
|
223
|
+
"operational"
|
|
224
|
+
]
|
|
225
|
+
},
|
|
226
|
+
"level": {
|
|
227
|
+
"enum": [
|
|
228
|
+
"required",
|
|
229
|
+
"recommended",
|
|
230
|
+
"optional"
|
|
231
|
+
]
|
|
232
|
+
},
|
|
233
|
+
"kind": {
|
|
234
|
+
"enum": [
|
|
235
|
+
"hard",
|
|
236
|
+
"soft"
|
|
237
|
+
]
|
|
238
|
+
},
|
|
239
|
+
"verification": {
|
|
240
|
+
"enum": [
|
|
241
|
+
"deterministic",
|
|
242
|
+
"semantic",
|
|
243
|
+
"human"
|
|
244
|
+
]
|
|
245
|
+
},
|
|
246
|
+
"command": {
|
|
247
|
+
"type": [
|
|
248
|
+
"string",
|
|
249
|
+
"null"
|
|
250
|
+
]
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
"profileId": {
|
|
255
|
+
"type": "string",
|
|
256
|
+
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
|
|
257
|
+
},
|
|
258
|
+
"guardrail": {
|
|
259
|
+
"type": "object",
|
|
260
|
+
"additionalProperties": false,
|
|
261
|
+
"required": [
|
|
262
|
+
"id",
|
|
263
|
+
"level",
|
|
264
|
+
"kind",
|
|
265
|
+
"verification",
|
|
266
|
+
"command",
|
|
267
|
+
"description"
|
|
268
|
+
],
|
|
269
|
+
"properties": {
|
|
270
|
+
"id": {
|
|
271
|
+
"type": "string",
|
|
272
|
+
"minLength": 1
|
|
273
|
+
},
|
|
274
|
+
"level": {
|
|
275
|
+
"enum": [
|
|
276
|
+
"required",
|
|
277
|
+
"recommended",
|
|
278
|
+
"optional"
|
|
279
|
+
]
|
|
280
|
+
},
|
|
281
|
+
"kind": {
|
|
282
|
+
"enum": [
|
|
283
|
+
"hard",
|
|
284
|
+
"soft"
|
|
285
|
+
]
|
|
286
|
+
},
|
|
287
|
+
"verification": {
|
|
288
|
+
"enum": [
|
|
289
|
+
"deterministic",
|
|
290
|
+
"semantic",
|
|
291
|
+
"human"
|
|
292
|
+
]
|
|
293
|
+
},
|
|
294
|
+
"command": {
|
|
295
|
+
"type": [
|
|
296
|
+
"string",
|
|
297
|
+
"null"
|
|
298
|
+
]
|
|
299
|
+
},
|
|
300
|
+
"description": {
|
|
301
|
+
"type": "string",
|
|
302
|
+
"minLength": 1
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
"combination": {
|
|
307
|
+
"type": "object",
|
|
308
|
+
"additionalProperties": false,
|
|
309
|
+
"required": [
|
|
310
|
+
"with",
|
|
311
|
+
"purpose"
|
|
312
|
+
],
|
|
313
|
+
"properties": {
|
|
314
|
+
"with": {
|
|
315
|
+
"type": "array",
|
|
316
|
+
"items": {
|
|
317
|
+
"$ref": "#/$defs/profileId"
|
|
318
|
+
},
|
|
319
|
+
"minItems": 1,
|
|
320
|
+
"uniqueItems": true
|
|
321
|
+
},
|
|
322
|
+
"purpose": {
|
|
323
|
+
"type": "string",
|
|
324
|
+
"minLength": 1
|
|
325
|
+
}
|
|
60
326
|
}
|
|
61
327
|
}
|
|
62
328
|
}
|
package/package.json
CHANGED
package/src/lighthouse.mjs
CHANGED
|
@@ -46,10 +46,15 @@ const managedPaths = [
|
|
|
46
46
|
const profileManagedPaths = Object.freeze({
|
|
47
47
|
'visual-theme': ['docs/design/DESIGN.md', '.agents/skills/lighthouse-ui/SKILL.md'],
|
|
48
48
|
'dotnet-backend': ['docs/profiles/dotnet/README.md', 'docs/profiles/dotnet/adoption.md', 'docs/profiles/dotnet/architecture.md', 'docs/profiles/dotnet/guardrails.md', 'docs/profiles/dotnet/practices.md', 'docs/profiles/dotnet/testing-and-development.md', 'docs/profiles/dotnet/use-cases.md', '.agents/skills/lighthouse-dotnet/SKILL.md'],
|
|
49
|
+
'react-web-platform': ['docs/profiles/react-web-platform/README.md', 'docs/profiles/react-web-platform/adoption.md', 'docs/profiles/react-web-platform/structure.md', 'docs/profiles/react-web-platform/guardrails.md', 'docs/profiles/react-web-platform/testing.md', '.agents/skills/lighthouse-react-web/SKILL.md'],
|
|
50
|
+
'web-bff': ['docs/profiles/web-bff/README.md', 'docs/profiles/web-bff/adoption.md', 'docs/profiles/web-bff/security.md', 'docs/profiles/web-bff/testing.md', '.agents/skills/lighthouse-web-bff/SKILL.md'],
|
|
51
|
+
'pwa-capability': ['docs/profiles/pwa-capability/README.md', 'docs/profiles/pwa-capability/adoption.md', 'docs/profiles/pwa-capability/guardrails.md', 'docs/profiles/pwa-capability/offline.md', 'docs/profiles/pwa-capability/testing.md', '.agents/skills/lighthouse-pwa/SKILL.md'],
|
|
52
|
+
'mobile-expo': ['docs/profiles/mobile-expo/README.md', 'docs/profiles/mobile-expo/adoption.md', 'docs/profiles/mobile-expo/guardrails.md', 'docs/profiles/mobile-expo/testing.md', '.agents/skills/lighthouse-mobile-expo/SKILL.md'],
|
|
53
|
+
'mobile-maui': ['docs/profiles/mobile-maui/README.md', 'docs/profiles/mobile-maui/adoption.md', 'docs/profiles/mobile-maui/guardrails.md', 'docs/profiles/mobile-maui/testing.md', '.agents/skills/lighthouse-mobile-maui/SKILL.md'],
|
|
49
54
|
});
|
|
50
55
|
const previousManagedPaths = managedPaths.slice(0, -1);
|
|
51
56
|
const legacyManagedPaths = [projection];
|
|
52
|
-
const supportedProfiles =
|
|
57
|
+
const supportedProfiles = readProfileCatalog().profiles.map(({ id }) => id);
|
|
53
58
|
const branchPattern = '^(feat|fix|docs|refactor|test|chore|build|ci|perf|revert)/[1-9]\\d*-[a-z0-9]+(?:-[a-z0-9]+)*$';
|
|
54
59
|
const authorityVersions = { productShape: '0.22.0', arcShape: '0.1.3' };
|
|
55
60
|
const upgradeSchema = './node_modules/@juangcarmona/lighthouse/assets/schemas/lighthouse-upgrade.schema.json';
|
|
@@ -96,6 +101,46 @@ function profileAssets(manifest) {
|
|
|
96
101
|
catch (error) { throw new Error(`Cannot read the installed Lighthouse .NET profile: ${error.message}`); }
|
|
97
102
|
}
|
|
98
103
|
}
|
|
104
|
+
if ((manifest?.profiles ?? []).includes('react-web-platform')) {
|
|
105
|
+
const files = ['README.md', 'adoption.md', 'structure.md', 'guardrails.md', 'testing.md', 'SKILL.md'];
|
|
106
|
+
for (const file of files) {
|
|
107
|
+
const target = file === 'SKILL.md' ? '.agents/skills/lighthouse-react-web/SKILL.md' : `docs/profiles/react-web-platform/${file}`;
|
|
108
|
+
try { assets[target] = readFileSync(new URL(`../assets/profiles/react-web-platform/${file}`, import.meta.url), 'utf8'); }
|
|
109
|
+
catch (error) { throw new Error(`Cannot read the installed Lighthouse React web profile: ${error.message}`); }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if ((manifest?.profiles ?? []).includes('web-bff')) {
|
|
113
|
+
const files = ['README.md', 'adoption.md', 'security.md', 'testing.md', 'SKILL.md'];
|
|
114
|
+
for (const file of files) {
|
|
115
|
+
const target = file === 'SKILL.md' ? '.agents/skills/lighthouse-web-bff/SKILL.md' : `docs/profiles/web-bff/${file}`;
|
|
116
|
+
try { assets[target] = readFileSync(new URL(`../assets/profiles/web-bff/${file}`, import.meta.url), 'utf8'); }
|
|
117
|
+
catch (error) { throw new Error(`Cannot read the installed Lighthouse web BFF profile: ${error.message}`); }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if ((manifest?.profiles ?? []).includes('pwa-capability')) {
|
|
121
|
+
const files = ['README.md', 'adoption.md', 'guardrails.md', 'offline.md', 'testing.md', 'SKILL.md'];
|
|
122
|
+
for (const file of files) {
|
|
123
|
+
const target = file === 'SKILL.md' ? '.agents/skills/lighthouse-pwa/SKILL.md' : `docs/profiles/pwa-capability/${file}`;
|
|
124
|
+
try { assets[target] = readFileSync(new URL(`../assets/profiles/pwa-capability/${file}`, import.meta.url), 'utf8'); }
|
|
125
|
+
catch (error) { throw new Error(`Cannot read the installed Lighthouse PWA capability: ${error.message}`); }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if ((manifest?.profiles ?? []).includes('mobile-expo')) {
|
|
129
|
+
const files = ['README.md', 'adoption.md', 'guardrails.md', 'testing.md', 'SKILL.md'];
|
|
130
|
+
for (const file of files) {
|
|
131
|
+
const target = file === 'SKILL.md' ? '.agents/skills/lighthouse-mobile-expo/SKILL.md' : `docs/profiles/mobile-expo/${file}`;
|
|
132
|
+
try { assets[target] = readFileSync(new URL(`../assets/profiles/mobile-expo/${file}`, import.meta.url), 'utf8'); }
|
|
133
|
+
catch (error) { throw new Error(`Cannot read the installed Lighthouse Expo mobile profile: ${error.message}`); }
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if ((manifest?.profiles ?? []).includes('mobile-maui')) {
|
|
137
|
+
const files = ['README.md', 'adoption.md', 'guardrails.md', 'testing.md', 'SKILL.md'];
|
|
138
|
+
for (const file of files) {
|
|
139
|
+
const target = file === 'SKILL.md' ? '.agents/skills/lighthouse-mobile-maui/SKILL.md' : `docs/profiles/mobile-maui/${file}`;
|
|
140
|
+
try { assets[target] = readFileSync(new URL(`../assets/profiles/mobile-maui/${file}`, import.meta.url), 'utf8'); }
|
|
141
|
+
catch (error) { throw new Error(`Cannot read the installed Lighthouse .NET MAUI mobile profile: ${error.message}`); }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
99
144
|
return assets;
|
|
100
145
|
}
|
|
101
146
|
|
package/src/profiles.mjs
CHANGED
|
@@ -7,12 +7,37 @@ const allowedLevels = new Set(['required', 'recommended', 'optional']);
|
|
|
7
7
|
const allowedKinds = new Set(['hard', 'soft']);
|
|
8
8
|
const allowedVerification = new Set(['deterministic', 'semantic', 'human']);
|
|
9
9
|
const allowedLayers = new Set(['static', 'unit', 'architecture', 'integration', 'functional', 'browser-e2e', 'operational']);
|
|
10
|
+
const profileIdPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
11
|
+
|
|
12
|
+
function stringArrayProblems(value, name, { required = false } = {}) {
|
|
13
|
+
if (!Array.isArray(value) || (required && value.length === 0) || value.some((item) => typeof item !== 'string' || !item)) {
|
|
14
|
+
return [`${name} must contain strings`];
|
|
15
|
+
}
|
|
16
|
+
return new Set(value).size === value.length ? [] : [`${name} must be unique`];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function profileReferenceProblems(value, name) {
|
|
20
|
+
return stringArrayProblems(value, name).concat((value ?? []).filter((id) => !profileIdPattern.test(id)).map((id) => `${name} contains invalid profile id ${id}`));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function guardrailProblems(guardrail) {
|
|
24
|
+
if (!guardrail || typeof guardrail !== 'object' || Array.isArray(guardrail)) return ['guardrail must be an object'];
|
|
25
|
+
const problems = [];
|
|
26
|
+
for (const key of ['id', 'level', 'kind', 'verification', 'command', 'description']) if (!(key in guardrail)) problems.push(`guardrail missing ${key}`);
|
|
27
|
+
if (typeof guardrail.id !== 'string' || !guardrail.id) problems.push('guardrail id must be a non-empty string');
|
|
28
|
+
if (!allowedLevels.has(guardrail.level)) problems.push(`guardrail ${guardrail.id} has an invalid level`);
|
|
29
|
+
if (!allowedKinds.has(guardrail.kind)) problems.push(`guardrail ${guardrail.id} has an invalid kind`);
|
|
30
|
+
if (!allowedVerification.has(guardrail.verification)) problems.push(`guardrail ${guardrail.id} has an invalid verification method`);
|
|
31
|
+
if (guardrail.command !== null && (typeof guardrail.command !== 'string' || !guardrail.command)) problems.push(`guardrail ${guardrail.id} command must be a string or null`);
|
|
32
|
+
if (typeof guardrail.description !== 'string' || !guardrail.description) problems.push(`guardrail ${guardrail.id} description must be a non-empty string`);
|
|
33
|
+
return problems;
|
|
34
|
+
}
|
|
10
35
|
|
|
11
36
|
function profileProblems(profile) {
|
|
12
37
|
const problems = [];
|
|
13
38
|
if (!profile || typeof profile !== 'object' || Array.isArray(profile)) return ['profile must be an object'];
|
|
14
39
|
for (const key of ['id', 'owner', 'purpose', 'appliesTo', 'surfaces', 'verification']) if (!(key in profile)) problems.push(`missing ${key}`);
|
|
15
|
-
if (typeof profile.id !== 'string' ||
|
|
40
|
+
if (typeof profile.id !== 'string' || !profileIdPattern.test(profile.id)) problems.push('id must use kebab-case');
|
|
16
41
|
if (typeof profile.owner !== 'string' || !profile.owner) problems.push('owner must be a non-empty string');
|
|
17
42
|
if (typeof profile.purpose !== 'string' || !profile.purpose) problems.push('purpose must be a non-empty string');
|
|
18
43
|
const applicability = profile.appliesTo;
|
|
@@ -24,6 +49,24 @@ function profileProblems(profile) {
|
|
|
24
49
|
if (Array.isArray(applicability?.shapes) && new Set(applicability.shapes).size !== applicability.shapes.length) problems.push('appliesTo.shapes must be unique');
|
|
25
50
|
if (Array.isArray(applicability?.stacks) && (applicability.stacks.some((stack) => typeof stack !== 'string') || new Set(applicability.stacks).size !== applicability.stacks.length)) problems.push('appliesTo.stacks must contain unique strings');
|
|
26
51
|
if (!Array.isArray(profile.surfaces) || profile.surfaces.length === 0 || profile.surfaces.some((surface) => typeof surface !== 'string' || !surface)) problems.push('surfaces must contain non-empty strings');
|
|
52
|
+
for (const [name, value] of [['prerequisites', profile.prerequisites], ['libraries', profile.libraries], ['structure', profile.structure], ['commands', profile.commands], ['repairHints', profile.repairHints], ['exceptions', profile.exceptions]]) problems.push(...stringArrayProblems(value, name, { required: ['repairHints', 'exceptions'].includes(name)}));
|
|
53
|
+
for (const [name, value] of [['dependencies', profile.dependencies], ['incompatibleWith', profile.incompatibleWith]]) problems.push(...profileReferenceProblems(value, name));
|
|
54
|
+
if (typeof profile.failureConsequence !== 'string' || !profile.failureConsequence) problems.push('failureConsequence must be a non-empty string');
|
|
55
|
+
if (typeof profile.consumerFixture !== 'string' || !profile.consumerFixture) problems.push('consumerFixture must be a non-empty string');
|
|
56
|
+
if (typeof profile.upgradeStrategy !== 'string' || !profile.upgradeStrategy) problems.push('upgradeStrategy must be a non-empty string');
|
|
57
|
+
if (!Array.isArray(profile.guardrails) || profile.guardrails.length === 0) problems.push('guardrails must contain at least one guardrail');
|
|
58
|
+
const guardrailIds = new Set();
|
|
59
|
+
for (const guardrail of profile.guardrails ?? []) {
|
|
60
|
+
problems.push(...guardrailProblems(guardrail));
|
|
61
|
+
if (guardrail?.id && guardrailIds.has(guardrail.id)) problems.push(`duplicate guardrail ${guardrail.id}`);
|
|
62
|
+
if (guardrail?.id) guardrailIds.add(guardrail.id);
|
|
63
|
+
}
|
|
64
|
+
if (!Array.isArray(profile.combinations)) problems.push('combinations must be an array');
|
|
65
|
+
for (const combination of profile.combinations ?? []) {
|
|
66
|
+
if (!combination || typeof combination !== 'object' || Array.isArray(combination)) { problems.push('combination must be an object'); continue; }
|
|
67
|
+
if (!Array.isArray(combination.with) || combination.with.length === 0 || combination.with.some((id) => typeof id !== 'string' || !profileIdPattern.test(id))) problems.push('combination.with must contain profile ids');
|
|
68
|
+
if (typeof combination.purpose !== 'string' || !combination.purpose) problems.push('combination.purpose must be a non-empty string');
|
|
69
|
+
}
|
|
27
70
|
const layers = profile.verification?.layers;
|
|
28
71
|
if (!Array.isArray(layers) || layers.length === 0) problems.push('verification.layers must contain at least one layer');
|
|
29
72
|
const ids = new Set();
|
|
@@ -52,6 +95,11 @@ function catalogProblems(catalog) {
|
|
|
52
95
|
if (profile?.id && ids.has(profile.id)) problems.push(`${profile.id}: duplicate id`);
|
|
53
96
|
if (profile?.id) ids.add(profile.id);
|
|
54
97
|
}
|
|
98
|
+
for (const profile of catalog.profiles ?? []) {
|
|
99
|
+
for (const reference of [...(profile.dependencies ?? []), ...(profile.incompatibleWith ?? []), ...(profile.combinations ?? []).flatMap((combination) => combination.with ?? [])]) {
|
|
100
|
+
if (!ids.has(reference)) problems.push(`${profile.id}: references undeclared profile ${reference}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
55
103
|
return problems;
|
|
56
104
|
}
|
|
57
105
|
|
|
@@ -80,31 +128,56 @@ function profileStatus(profile, application) {
|
|
|
80
128
|
return 'passed';
|
|
81
129
|
}
|
|
82
130
|
|
|
131
|
+
function profileCompositionProblems(profile, selectedIds) {
|
|
132
|
+
const problems = [];
|
|
133
|
+
for (const dependency of profile.dependencies ?? []) if (!selectedIds.has(dependency)) problems.push(`${profile.id} requires the ${dependency} profile.`);
|
|
134
|
+
for (const incompatible of profile.incompatibleWith ?? []) if (selectedIds.has(incompatible)) problems.push(`${profile.id} is incompatible with the ${incompatible} profile.`);
|
|
135
|
+
return problems;
|
|
136
|
+
}
|
|
137
|
+
|
|
83
138
|
export function composeProfiles(catalog, manifest) {
|
|
84
139
|
const application = normalizeApplication(manifest?.application);
|
|
140
|
+
const selectedIds = new Set(manifest?.profiles ?? []);
|
|
85
141
|
const selected = (manifest?.profiles ?? []).map((id) => {
|
|
86
142
|
const profile = catalog.profiles.find((item) => item.id === id);
|
|
87
143
|
const status = profile ? profileStatus(profile, application) : 'failed';
|
|
144
|
+
const compositionProblems = profile ? profileCompositionProblems(profile, selectedIds) : [];
|
|
88
145
|
const problems = profile
|
|
89
146
|
? status === 'failed'
|
|
90
147
|
? [`${id} does not apply to application shape '${application.shape}' and stacks '${application.stacks.join(', ') || 'none'}'.`]
|
|
91
148
|
: status === 'unconfigured'
|
|
92
149
|
? [`${id} needs application.shape and${profile.appliesTo.stacks.length ? ' application.stacks' : ''} in lighthouse.json.`]
|
|
93
|
-
:
|
|
150
|
+
: compositionProblems
|
|
94
151
|
: [`${id} is not declared by the installed profile catalog.`];
|
|
152
|
+
const effectiveStatus = status === 'failed' || compositionProblems.length ? 'failed' : status;
|
|
95
153
|
return {
|
|
96
154
|
id,
|
|
97
155
|
owner: profile?.owner ?? '@juangcarmona/lighthouse',
|
|
98
156
|
purpose: profile?.purpose ?? null,
|
|
99
|
-
status,
|
|
100
157
|
appliesTo: profile?.appliesTo ?? null,
|
|
101
158
|
surfaces: profile?.surfaces ?? [],
|
|
159
|
+
prerequisites: profile?.prerequisites ?? [],
|
|
160
|
+
dependencies: profile?.dependencies ?? [],
|
|
161
|
+
incompatibleWith: profile?.incompatibleWith ?? [],
|
|
162
|
+
libraries: profile?.libraries ?? [],
|
|
163
|
+
structure: profile?.structure ?? [],
|
|
164
|
+
commands: profile?.commands ?? [],
|
|
165
|
+
guardrails: profile?.guardrails ?? [],
|
|
166
|
+
failureConsequence: profile?.failureConsequence ?? null,
|
|
167
|
+
repairHints: profile?.repairHints ?? [],
|
|
168
|
+
exceptions: profile?.exceptions ?? [],
|
|
169
|
+
combinations: profile?.combinations ?? [],
|
|
170
|
+
consumerFixture: profile?.consumerFixture ?? null,
|
|
171
|
+
upgradeStrategy: profile?.upgradeStrategy ?? null,
|
|
102
172
|
verification: profile?.verification ?? { layers: [] },
|
|
103
173
|
problems,
|
|
174
|
+
status: effectiveStatus,
|
|
104
175
|
nextAction: problems.length
|
|
105
176
|
? status === 'unconfigured'
|
|
106
177
|
? 'Declare application.shape and application.stacks, then run lighthouse profiles --format json.'
|
|
107
|
-
:
|
|
178
|
+
: compositionProblems.length
|
|
179
|
+
? 'Select the required profiles or remove the incompatible selection, then rerun lighthouse profiles --format json.'
|
|
180
|
+
: 'Choose a compatible profile or correct application.shape and application.stacks.'
|
|
108
181
|
: null,
|
|
109
182
|
};
|
|
110
183
|
});
|
|
@@ -115,7 +188,7 @@ export function composeProfiles(catalog, manifest) {
|
|
|
115
188
|
: selected.some((profile) => profile.status === 'unconfigured')
|
|
116
189
|
? 'unconfigured'
|
|
117
190
|
: 'passed';
|
|
118
|
-
return { schemaVersion: 1, application, selected, available: catalog.profiles.map((
|
|
191
|
+
return { schemaVersion: 1, application, selected, available: catalog.profiles.map((profile) => ({ ...profile })), status };
|
|
119
192
|
}
|
|
120
193
|
|
|
121
194
|
export function profileCompositionResult(composition) {
|