@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.
Files changed (32) hide show
  1. package/assets/profiles/catalog.json +1019 -17
  2. package/assets/profiles/mobile-expo/README.md +19 -0
  3. package/assets/profiles/mobile-expo/SKILL.md +14 -0
  4. package/assets/profiles/mobile-expo/adoption.md +11 -0
  5. package/assets/profiles/mobile-expo/guardrails.md +14 -0
  6. package/assets/profiles/mobile-expo/testing.md +13 -0
  7. package/assets/profiles/mobile-maui/README.md +19 -0
  8. package/assets/profiles/mobile-maui/SKILL.md +14 -0
  9. package/assets/profiles/mobile-maui/adoption.md +11 -0
  10. package/assets/profiles/mobile-maui/guardrails.md +13 -0
  11. package/assets/profiles/mobile-maui/testing.md +13 -0
  12. package/assets/profiles/pwa-capability/README.md +19 -0
  13. package/assets/profiles/pwa-capability/SKILL.md +14 -0
  14. package/assets/profiles/pwa-capability/adoption.md +9 -0
  15. package/assets/profiles/pwa-capability/guardrails.md +13 -0
  16. package/assets/profiles/pwa-capability/offline.md +11 -0
  17. package/assets/profiles/pwa-capability/testing.md +15 -0
  18. package/assets/profiles/react-web-platform/README.md +24 -0
  19. package/assets/profiles/react-web-platform/SKILL.md +18 -0
  20. package/assets/profiles/react-web-platform/adoption.md +17 -0
  21. package/assets/profiles/react-web-platform/guardrails.md +17 -0
  22. package/assets/profiles/react-web-platform/structure.md +36 -0
  23. package/assets/profiles/react-web-platform/testing.md +15 -0
  24. package/assets/profiles/web-bff/README.md +22 -0
  25. package/assets/profiles/web-bff/SKILL.md +16 -0
  26. package/assets/profiles/web-bff/adoption.md +9 -0
  27. package/assets/profiles/web-bff/security.md +16 -0
  28. package/assets/profiles/web-bff/testing.md +14 -0
  29. package/assets/schemas/profile-catalog.schema.json +286 -20
  30. package/package.json +1 -1
  31. package/src/lighthouse.mjs +46 -1
  32. 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": ["$schema", "schemaVersion", "profiles"],
7
+ "required": [
8
+ "$schema",
9
+ "schemaVersion",
10
+ "profiles"
11
+ ],
8
12
  "properties": {
9
- "$schema": { "type": "string" },
10
- "schemaVersion": { "const": 1 },
13
+ "$schema": {
14
+ "type": "string"
15
+ },
16
+ "schemaVersion": {
17
+ "const": 1
18
+ },
11
19
  "profiles": {
12
20
  "type": "array",
13
- "items": { "$ref": "#/$defs/profile" }
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": ["id", "owner", "purpose", "appliesTo", "surfaces", "verification"],
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": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" },
23
- "owner": { "type": "string", "minLength": 1 },
24
- "purpose": { "type": "string", "minLength": 1 },
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": ["shapes", "stacks"],
67
+ "required": [
68
+ "shapes",
69
+ "stacks"
70
+ ],
29
71
  "properties": {
30
- "shapes": { "type": "array", "items": { "type": "string" }, "minItems": 1, "uniqueItems": true },
31
- "stacks": { "type": "array", "items": { "type": "string" }, "uniqueItems": true }
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": { "type": "array", "items": { "type": "string", "minLength": 1 }, "minItems": 1, "uniqueItems": true },
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": ["layers"],
101
+ "required": [
102
+ "layers"
103
+ ],
39
104
  "properties": {
40
105
  "layers": {
41
106
  "type": "array",
42
- "items": { "$ref": "#/$defs/layer" },
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": ["id", "level", "kind", "verification"],
208
+ "required": [
209
+ "id",
210
+ "level",
211
+ "kind",
212
+ "verification"
213
+ ],
54
214
  "properties": {
55
- "id": { "enum": ["static", "unit", "architecture", "integration", "functional", "browser-e2e", "operational"] },
56
- "level": { "enum": ["required", "recommended", "optional"] },
57
- "kind": { "enum": ["hard", "soft"] },
58
- "verification": { "enum": ["deterministic", "semantic", "human"] },
59
- "command": { "type": ["string", "null"] }
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juangcarmona/lighthouse",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "Lighthouse adoption and verification CLI",
6
6
  "repository": {
@@ -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 = ['node-tooling', 'dotnet-backend', 'visual-theme'];
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' || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(profile.id)) problems.push('id must use kebab-case');
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
- : 'Choose a compatible profile or correct application.shape and application.stacks.'
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(({ id, owner, purpose, appliesTo, surfaces, verification }) => ({ id, owner, purpose, appliesTo, surfaces, verification })), status };
191
+ return { schemaVersion: 1, application, selected, available: catalog.profiles.map((profile) => ({ ...profile })), status };
119
192
  }
120
193
 
121
194
  export function profileCompositionResult(composition) {