@juangcarmona/lighthouse 0.5.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/README.md +8 -2
- package/assets/dotnet-template/Directory.Build.props +22 -0
- package/assets/dotnet-template/Directory.Packages.props +16 -0
- package/assets/dotnet-template/Lighthouse.Analyzers/BackendContractAnalyzer.cs +72 -0
- package/assets/dotnet-template/Lighthouse.Analyzers/Lighthouse.Analyzers.csproj +12 -0
- package/assets/dotnet-template/Lighthouse.Analyzers/packages.lock.json +121 -0
- package/assets/dotnet-template/Lighthouse.Cqrs/Handlers.cs +11 -0
- package/assets/dotnet-template/Lighthouse.Cqrs/Lighthouse.Cqrs.csproj +5 -0
- package/assets/dotnet-template/Lighthouse.Cqrs/packages.lock.json +6 -0
- package/assets/dotnet-template/Lighthouse.slnx +17 -0
- package/assets/dotnet-template/examples/src/modules/Lighthouse.Example.Modules.Catalog/CatalogModule.cs +24 -0
- package/assets/dotnet-template/examples/src/modules/Lighthouse.Example.Modules.Catalog/Features/Entries/CreateEntry.cs +47 -0
- package/assets/dotnet-template/examples/src/modules/Lighthouse.Example.Modules.Catalog/Features/Entries/EntryStore.cs +17 -0
- package/assets/dotnet-template/examples/src/modules/Lighthouse.Example.Modules.Catalog/Features/Entries/GetEntry.cs +30 -0
- package/assets/dotnet-template/examples/src/modules/Lighthouse.Example.Modules.Catalog/Lighthouse.Example.Modules.Catalog.csproj +12 -0
- package/assets/dotnet-template/examples/src/modules/Lighthouse.Example.Modules.Catalog/packages.lock.json +10 -0
- package/assets/dotnet-template/examples/src/root/Lighthouse.Example.Api/Lighthouse.Example.Api.csproj +6 -0
- package/assets/dotnet-template/examples/src/root/Lighthouse.Example.Api/Program.cs +25 -0
- package/assets/dotnet-template/examples/src/root/Lighthouse.Example.Api/Properties/launchSettings.json +10 -0
- package/assets/dotnet-template/examples/src/root/Lighthouse.Example.Api/packages.lock.json +86 -0
- package/assets/dotnet-template/examples/src/root/Lighthouse.Example.AppHost/Lighthouse.Example.AppHost.csproj +13 -0
- package/assets/dotnet-template/examples/src/root/Lighthouse.Example.AppHost/Program.cs +3 -0
- package/assets/dotnet-template/examples/src/root/Lighthouse.Example.AppHost/Properties/launchSettings.json +15 -0
- package/assets/dotnet-template/examples/src/root/Lighthouse.Example.AppHost/aspire.config.json +5 -0
- package/assets/dotnet-template/examples/src/root/Lighthouse.Example.AppHost/packages.lock.json +689 -0
- package/assets/dotnet-template/global.json +6 -0
- package/assets/dotnet-template/tests/Lighthouse.Analyzers.Tests/BackendContractTests.cs +102 -0
- package/assets/dotnet-template/tests/Lighthouse.Analyzers.Tests/Lighthouse.Analyzers.Tests.csproj +9 -0
- package/assets/dotnet-template/tests/Lighthouse.Analyzers.Tests/packages.lock.json +125 -0
- package/assets/dotnet-template/tests/Lighthouse.Example.Tests/CatalogTests.cs +111 -0
- package/assets/dotnet-template/tests/Lighthouse.Example.Tests/Lighthouse.Example.Tests.csproj +9 -0
- package/assets/dotnet-template/tests/Lighthouse.Example.Tests/packages.lock.json +490 -0
- package/assets/profiles/catalog.json +1050 -0
- package/assets/profiles/dotnet/README.md +32 -0
- package/assets/profiles/dotnet/SKILL.md +18 -0
- package/assets/profiles/dotnet/adoption.md +39 -0
- package/assets/profiles/dotnet/architecture.md +37 -0
- package/assets/profiles/dotnet/guardrails.md +32 -0
- package/assets/profiles/dotnet/practices.md +36 -0
- package/assets/profiles/dotnet/testing-and-development.md +29 -0
- package/assets/profiles/dotnet/use-cases.md +29 -0
- 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/lighthouse-manifest.schema.json +10 -7
- package/assets/schemas/profile-catalog.schema.json +329 -0
- package/package.json +5 -2
- package/src/create-dotnet.mjs +112 -0
- package/src/lighthouse.mjs +109 -14
- package/src/profiles.mjs +206 -0
- package/src/verify-dotnet.mjs +85 -0
package/src/lighthouse.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { createRequire } from 'node:module';
|
|
|
6
6
|
import { basename, dirname, resolve } from 'node:path';
|
|
7
7
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
8
8
|
import { evaluateGuardrails, readGuardrailCatalog } from './guardrails.mjs';
|
|
9
|
+
import { composeProfiles, normalizeApplication, profileCompositionResult, readProfileCatalog } from './profiles.mjs';
|
|
9
10
|
let sdlcCapabilities;
|
|
10
11
|
let sdlcReconcile;
|
|
11
12
|
let sdlcGithub;
|
|
@@ -44,14 +45,20 @@ const managedPaths = [
|
|
|
44
45
|
];
|
|
45
46
|
const profileManagedPaths = Object.freeze({
|
|
46
47
|
'visual-theme': ['docs/design/DESIGN.md', '.agents/skills/lighthouse-ui/SKILL.md'],
|
|
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'],
|
|
47
54
|
});
|
|
48
55
|
const previousManagedPaths = managedPaths.slice(0, -1);
|
|
49
56
|
const legacyManagedPaths = [projection];
|
|
50
|
-
const supportedProfiles =
|
|
57
|
+
const supportedProfiles = readProfileCatalog().profiles.map(({ id }) => id);
|
|
51
58
|
const branchPattern = '^(feat|fix|docs|refactor|test|chore|build|ci|perf|revert)/[1-9]\\d*-[a-z0-9]+(?:-[a-z0-9]+)*$';
|
|
52
59
|
const authorityVersions = { productShape: '0.22.0', arcShape: '0.1.3' };
|
|
53
60
|
const upgradeSchema = './node_modules/@juangcarmona/lighthouse/assets/schemas/lighthouse-upgrade.schema.json';
|
|
54
|
-
const usage = 'Usage: lighthouse <init|authorities|reconcile|upgrade|capabilities|verify> [--name <product>] [--profile <profile>] [--scope <harness|sdlc|all>] [--format <text|json>] [--dry-run] [--apply]';
|
|
61
|
+
const usage = 'Usage: lighthouse <init|create-dotnet|profiles|authorities|reconcile|upgrade|capabilities|verify> [--name <product>] [--profile <profile>] [--scope <harness|sdlc|all>] [--format <text|json>] [--dry-run] [--apply]';
|
|
55
62
|
|
|
56
63
|
const defaultRepository = () => ({
|
|
57
64
|
layout: [...layout],
|
|
@@ -77,15 +84,64 @@ function packageAsset(packageName, entry) {
|
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
function profileAssets(manifest) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
'docs/design/DESIGN.md'
|
|
84
|
-
'.agents/skills/lighthouse-ui/SKILL.md'
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
const assets = {};
|
|
88
|
+
if ((manifest?.profiles ?? []).includes('visual-theme')) {
|
|
89
|
+
try {
|
|
90
|
+
assets['docs/design/DESIGN.md'] = packageAsset('@juangcarmona/lighthouse-ui', 'DESIGN.md');
|
|
91
|
+
assets['.agents/skills/lighthouse-ui/SKILL.md'] = packageAsset('@juangcarmona/lighthouse-ui', 'skills/ui-design/SKILL.md');
|
|
92
|
+
} catch (error) {
|
|
93
|
+
throw new Error(`Cannot read the installed Lighthouse UI profile: ${error.message}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if ((manifest?.profiles ?? []).includes('dotnet-backend')) {
|
|
97
|
+
const files = ['README.md', 'adoption.md', 'architecture.md', 'guardrails.md', 'practices.md', 'testing-and-development.md', 'use-cases.md', 'SKILL.md'];
|
|
98
|
+
for (const file of files) {
|
|
99
|
+
const target = file === 'SKILL.md' ? '.agents/skills/lighthouse-dotnet/SKILL.md' : `docs/profiles/dotnet/${file}`;
|
|
100
|
+
try { assets[target] = readFileSync(new URL(`../assets/profiles/dotnet/${file}`, import.meta.url), 'utf8'); }
|
|
101
|
+
catch (error) { throw new Error(`Cannot read the installed Lighthouse .NET profile: ${error.message}`); }
|
|
102
|
+
}
|
|
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
|
+
}
|
|
88
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
|
+
}
|
|
144
|
+
return assets;
|
|
89
145
|
}
|
|
90
146
|
|
|
91
147
|
function profileArguments(args) {
|
|
@@ -108,6 +164,7 @@ function defaultManifest(product, profiles = []) {
|
|
|
108
164
|
schemaVersion: 1,
|
|
109
165
|
baseline: '0.1.0',
|
|
110
166
|
product,
|
|
167
|
+
application: { shape: 'unknown', stacks: [] },
|
|
111
168
|
profiles: [...profiles],
|
|
112
169
|
integrations: { productShape: null, arcShape: null },
|
|
113
170
|
packages,
|
|
@@ -458,7 +515,7 @@ function printUpgradePlan(plan) {
|
|
|
458
515
|
function validate(manifest) {
|
|
459
516
|
const problems = [];
|
|
460
517
|
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) return ['manifest must be an object'];
|
|
461
|
-
const allowed = ['$schema', 'schemaVersion', 'baseline', 'product', 'profiles', 'integrations', 'packages', 'managed', 'repository'];
|
|
518
|
+
const allowed = ['$schema', 'schemaVersion', 'baseline', 'product', 'application', 'profiles', 'integrations', 'packages', 'managed', 'repository'];
|
|
462
519
|
for (const key of Object.keys(manifest)) if (!allowed.includes(key)) problems.push(`unknown field ${key}`);
|
|
463
520
|
for (const key of ['schemaVersion', 'baseline', 'product', 'profiles', 'integrations', 'managed']) {
|
|
464
521
|
if (!(key in manifest)) problems.push(`missing ${key}`);
|
|
@@ -469,6 +526,15 @@ function validate(manifest) {
|
|
|
469
526
|
if (typeof manifest.product !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,63}$/.test(manifest.product)) {
|
|
470
527
|
problems.push('product must be a readable name of 1–64 characters');
|
|
471
528
|
}
|
|
529
|
+
if ('application' in manifest) {
|
|
530
|
+
const application = manifest.application;
|
|
531
|
+
if (!application || typeof application !== 'object' || Array.isArray(application)
|
|
532
|
+
|| !['unknown', 'frontend', 'backend', 'full-stack', 'service', 'library', 'tooling', 'cli', 'worker', 'mobile'].includes(application.shape)
|
|
533
|
+
|| !Array.isArray(application.stacks) || application.stacks.some((stack) => typeof stack !== 'string' || !stack)
|
|
534
|
+
|| new Set(application.stacks).size !== application.stacks.length) {
|
|
535
|
+
problems.push('application must declare a supported shape and unique stack names');
|
|
536
|
+
}
|
|
537
|
+
}
|
|
472
538
|
const profiles = supportedProfiles;
|
|
473
539
|
if (!Array.isArray(manifest.profiles) || manifest.profiles.some((p) => !profiles.includes(p))
|
|
474
540
|
|| new Set(manifest.profiles).size !== manifest.profiles.length) {
|
|
@@ -923,6 +989,11 @@ async function verify(manifest, format = 'text') {
|
|
|
923
989
|
branch: gitBranch(),
|
|
924
990
|
uiTokens: manifest?.profiles?.includes('visual-theme') ? packageAsset('@juangcarmona/lighthouse-ui', 'tokens.css') : null,
|
|
925
991
|
});
|
|
992
|
+
const profileComposition = composeProfiles(readProfileCatalog(), manifest);
|
|
993
|
+
report.profiles = profileComposition;
|
|
994
|
+
const profileResult = profileCompositionResult(profileComposition);
|
|
995
|
+
report.results.push(profileResult);
|
|
996
|
+
report.summary[profileResult.status] = (report.summary[profileResult.status] ?? 0) + 1;
|
|
926
997
|
const adoption = Object.values(capabilityAdoption(await readUpgradeState()));
|
|
927
998
|
for (const item of adoption) {
|
|
928
999
|
if (item.status !== 'adopted' || !item.recordPath || await exists(resolve(root, item.recordPath))) continue;
|
|
@@ -956,6 +1027,20 @@ async function verify(manifest, format = 'text') {
|
|
|
956
1027
|
console.log('Lighthouse manifest, repository boundaries and managed projection are valid.');
|
|
957
1028
|
}
|
|
958
1029
|
|
|
1030
|
+
async function profiles(manifest, format = 'text') {
|
|
1031
|
+
const composition = composeProfiles(readProfileCatalog(), manifest);
|
|
1032
|
+
if (format === 'json') {
|
|
1033
|
+
console.log(JSON.stringify(composition, null, 2));
|
|
1034
|
+
if (composition.status === 'failed') process.exitCode = 1;
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
console.log(`Lighthouse profile composition: ${composition.status}`);
|
|
1038
|
+
console.log(` application ${composition.application.shape}${composition.application.stacks.length ? ` (${composition.application.stacks.join(', ')})` : ''}`);
|
|
1039
|
+
for (const profile of composition.selected) console.log(` ${profile.id}: ${profile.status}${profile.nextAction ? `; next: ${profile.nextAction}` : ''}`);
|
|
1040
|
+
if (!composition.selected.length) console.log(' no profiles selected');
|
|
1041
|
+
if (composition.status === 'failed') process.exitCode = 1;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
959
1044
|
function planHash(plan) {
|
|
960
1045
|
const { authorization, ...payload } = plan;
|
|
961
1046
|
return contentHash(JSON.stringify(payload));
|
|
@@ -1117,6 +1202,11 @@ function formatFromArgs(args) {
|
|
|
1117
1202
|
const args = process.argv.slice(2);
|
|
1118
1203
|
const command = args[0];
|
|
1119
1204
|
const commandArgs = args.slice(1);
|
|
1205
|
+
if (command === 'create-dotnet') {
|
|
1206
|
+
process.argv.splice(2, 1);
|
|
1207
|
+
await import('./create-dotnet.mjs');
|
|
1208
|
+
process.exit(0);
|
|
1209
|
+
}
|
|
1120
1210
|
let invalidArgument = false;
|
|
1121
1211
|
for (let index = 0; index < commandArgs.length; index += 1) {
|
|
1122
1212
|
const arg = commandArgs[index];
|
|
@@ -1135,12 +1225,12 @@ for (let index = 0; index < commandArgs.length; index += 1) {
|
|
|
1135
1225
|
continue;
|
|
1136
1226
|
}
|
|
1137
1227
|
if (arg === '--format') {
|
|
1138
|
-
if (!['verify', 'upgrade', 'capabilities'].includes(command) || !['text', 'json'].includes(commandArgs[index + 1])) invalidArgument = true;
|
|
1228
|
+
if (!['profiles', 'verify', 'upgrade', 'capabilities'].includes(command) || !['text', 'json'].includes(commandArgs[index + 1])) invalidArgument = true;
|
|
1139
1229
|
else index += 1;
|
|
1140
1230
|
continue;
|
|
1141
1231
|
}
|
|
1142
1232
|
if (arg.startsWith('--format=') && ['text', 'json'].includes(arg.slice('--format='.length))) {
|
|
1143
|
-
if (!['verify', 'upgrade', 'capabilities'].includes(command)) invalidArgument = true;
|
|
1233
|
+
if (!['profiles', 'verify', 'upgrade', 'capabilities'].includes(command)) invalidArgument = true;
|
|
1144
1234
|
continue;
|
|
1145
1235
|
}
|
|
1146
1236
|
if (arg === '--scope') {
|
|
@@ -1160,7 +1250,7 @@ for (let index = 0; index < commandArgs.length; index += 1) {
|
|
|
1160
1250
|
if (['--id=', '--provider=', '--repo=', '--input=', '--plan-file=', '--authorize='].some((prefix) => arg.startsWith(prefix) && arg.length > prefix.length)) continue;
|
|
1161
1251
|
invalidArgument = true;
|
|
1162
1252
|
}
|
|
1163
|
-
if (!['init', 'authorities', 'reconcile', 'upgrade', 'capabilities', 'verify'].includes(command) || invalidArgument) {
|
|
1253
|
+
if (!['init', 'profiles', 'authorities', 'reconcile', 'upgrade', 'capabilities', 'verify'].includes(command) || invalidArgument) {
|
|
1164
1254
|
console.error(usage);
|
|
1165
1255
|
process.exit(2);
|
|
1166
1256
|
}
|
|
@@ -1170,6 +1260,11 @@ try {
|
|
|
1170
1260
|
else if (command === 'reconcile') await reconcile(args.slice(1));
|
|
1171
1261
|
else if (command === 'upgrade') await upgrade(args.slice(1));
|
|
1172
1262
|
else if (command === 'capabilities') await capabilities(args.slice(1));
|
|
1263
|
+
else if (command === 'profiles') {
|
|
1264
|
+
const manifest = await readManifest();
|
|
1265
|
+
if (!manifest) throw new Error('Cannot read lighthouse.json: file does not exist; run lighthouse init --name <product>.');
|
|
1266
|
+
await profiles(manifest, formatFromArgs(commandArgs));
|
|
1267
|
+
}
|
|
1173
1268
|
else {
|
|
1174
1269
|
const manifest = await readManifest();
|
|
1175
1270
|
if (!manifest) throw new Error('Cannot read lighthouse.json: file does not exist; run lighthouse init --name <product>.');
|
package/src/profiles.mjs
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
|
|
4
|
+
const catalogPath = fileURLToPath(new URL('../assets/profiles/catalog.json', import.meta.url));
|
|
5
|
+
const allowedShapes = new Set(['unknown', 'frontend', 'backend', 'full-stack', 'service', 'library', 'tooling', 'cli', 'worker', 'mobile']);
|
|
6
|
+
const allowedLevels = new Set(['required', 'recommended', 'optional']);
|
|
7
|
+
const allowedKinds = new Set(['hard', 'soft']);
|
|
8
|
+
const allowedVerification = new Set(['deterministic', 'semantic', 'human']);
|
|
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
|
+
}
|
|
35
|
+
|
|
36
|
+
function profileProblems(profile) {
|
|
37
|
+
const problems = [];
|
|
38
|
+
if (!profile || typeof profile !== 'object' || Array.isArray(profile)) return ['profile must be an object'];
|
|
39
|
+
for (const key of ['id', 'owner', 'purpose', 'appliesTo', 'surfaces', 'verification']) if (!(key in profile)) problems.push(`missing ${key}`);
|
|
40
|
+
if (typeof profile.id !== 'string' || !profileIdPattern.test(profile.id)) problems.push('id must use kebab-case');
|
|
41
|
+
if (typeof profile.owner !== 'string' || !profile.owner) problems.push('owner must be a non-empty string');
|
|
42
|
+
if (typeof profile.purpose !== 'string' || !profile.purpose) problems.push('purpose must be a non-empty string');
|
|
43
|
+
const applicability = profile.appliesTo;
|
|
44
|
+
if (!applicability || typeof applicability !== 'object' || Array.isArray(applicability)
|
|
45
|
+
|| !Array.isArray(applicability.shapes) || !Array.isArray(applicability.stacks)
|
|
46
|
+
|| applicability.shapes.length === 0 || applicability.shapes.some((shape) => typeof shape !== 'string' || !allowedShapes.has(shape))) {
|
|
47
|
+
problems.push('appliesTo must declare supported application shapes');
|
|
48
|
+
}
|
|
49
|
+
if (Array.isArray(applicability?.shapes) && new Set(applicability.shapes).size !== applicability.shapes.length) problems.push('appliesTo.shapes must be unique');
|
|
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');
|
|
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
|
+
}
|
|
70
|
+
const layers = profile.verification?.layers;
|
|
71
|
+
if (!Array.isArray(layers) || layers.length === 0) problems.push('verification.layers must contain at least one layer');
|
|
72
|
+
const ids = new Set();
|
|
73
|
+
for (const layer of layers ?? []) {
|
|
74
|
+
if (!layer || typeof layer !== 'object' || Array.isArray(layer)) { problems.push('verification layer must be an object'); continue; }
|
|
75
|
+
for (const key of ['id', 'level', 'kind', 'verification']) if (!(key in layer)) problems.push(`verification layer missing ${key}`);
|
|
76
|
+
if (!allowedLayers.has(layer.id)) problems.push(`unsupported verification layer ${layer.id}`);
|
|
77
|
+
if (!allowedLevels.has(layer.level)) problems.push(`verification layer ${layer.id} has an invalid level`);
|
|
78
|
+
if (!allowedKinds.has(layer.kind)) problems.push(`verification layer ${layer.id} has an invalid kind`);
|
|
79
|
+
if (!allowedVerification.has(layer.verification)) problems.push(`verification layer ${layer.id} has an invalid verification method`);
|
|
80
|
+
if (layer.command !== undefined && layer.command !== null && (typeof layer.command !== 'string' || !layer.command)) problems.push(`verification layer ${layer.id} command must be a string or null`);
|
|
81
|
+
if (ids.has(layer.id)) problems.push(`duplicate verification layer ${layer.id}`);
|
|
82
|
+
ids.add(layer.id);
|
|
83
|
+
}
|
|
84
|
+
return problems;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function catalogProblems(catalog) {
|
|
88
|
+
if (!catalog || typeof catalog !== 'object' || Array.isArray(catalog)) return ['catalog must be an object'];
|
|
89
|
+
const problems = [];
|
|
90
|
+
if (catalog.schemaVersion !== 1) problems.push('schemaVersion must be 1');
|
|
91
|
+
if (!Array.isArray(catalog.profiles)) problems.push('profiles must be an array');
|
|
92
|
+
const ids = new Set();
|
|
93
|
+
for (const profile of catalog.profiles ?? []) {
|
|
94
|
+
problems.push(...profileProblems(profile).map((problem) => `${profile?.id ?? '<unknown>'}: ${problem}`));
|
|
95
|
+
if (profile?.id && ids.has(profile.id)) problems.push(`${profile.id}: duplicate id`);
|
|
96
|
+
if (profile?.id) ids.add(profile.id);
|
|
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
|
+
}
|
|
103
|
+
return problems;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function readProfileCatalog() {
|
|
107
|
+
const catalog = JSON.parse(readFileSync(catalogPath, 'utf8'));
|
|
108
|
+
const problems = catalogProblems(catalog);
|
|
109
|
+
if (problems.length) throw new Error(`Invalid profile catalog:\n${problems.join('\n')}`);
|
|
110
|
+
return catalog;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function normalizeApplication(application) {
|
|
114
|
+
if (!application || typeof application !== 'object' || Array.isArray(application)) return { shape: 'unknown', stacks: [] };
|
|
115
|
+
return {
|
|
116
|
+
shape: typeof application.shape === 'string' ? application.shape : 'unknown',
|
|
117
|
+
stacks: Array.isArray(application.stacks) ? [...application.stacks] : [],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function profileStatus(profile, application) {
|
|
122
|
+
const shapeKnown = application.shape !== 'unknown';
|
|
123
|
+
const stacksKnown = application.stacks.length > 0;
|
|
124
|
+
const shapeMatches = !shapeKnown || profile.appliesTo.shapes.includes('any') || profile.appliesTo.shapes.includes(application.shape);
|
|
125
|
+
const stackMatches = !stacksKnown || profile.appliesTo.stacks.length === 0 || profile.appliesTo.stacks.includes('any') || application.stacks.some((stack) => profile.appliesTo.stacks.includes(stack));
|
|
126
|
+
if (!shapeMatches || !stackMatches) return 'failed';
|
|
127
|
+
if ((!shapeKnown && profile.appliesTo.shapes.length > 0) || (profile.appliesTo.stacks.length > 0 && !stacksKnown)) return 'unconfigured';
|
|
128
|
+
return 'passed';
|
|
129
|
+
}
|
|
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
|
+
|
|
138
|
+
export function composeProfiles(catalog, manifest) {
|
|
139
|
+
const application = normalizeApplication(manifest?.application);
|
|
140
|
+
const selectedIds = new Set(manifest?.profiles ?? []);
|
|
141
|
+
const selected = (manifest?.profiles ?? []).map((id) => {
|
|
142
|
+
const profile = catalog.profiles.find((item) => item.id === id);
|
|
143
|
+
const status = profile ? profileStatus(profile, application) : 'failed';
|
|
144
|
+
const compositionProblems = profile ? profileCompositionProblems(profile, selectedIds) : [];
|
|
145
|
+
const problems = profile
|
|
146
|
+
? status === 'failed'
|
|
147
|
+
? [`${id} does not apply to application shape '${application.shape}' and stacks '${application.stacks.join(', ') || 'none'}'.`]
|
|
148
|
+
: status === 'unconfigured'
|
|
149
|
+
? [`${id} needs application.shape and${profile.appliesTo.stacks.length ? ' application.stacks' : ''} in lighthouse.json.`]
|
|
150
|
+
: compositionProblems
|
|
151
|
+
: [`${id} is not declared by the installed profile catalog.`];
|
|
152
|
+
const effectiveStatus = status === 'failed' || compositionProblems.length ? 'failed' : status;
|
|
153
|
+
return {
|
|
154
|
+
id,
|
|
155
|
+
owner: profile?.owner ?? '@juangcarmona/lighthouse',
|
|
156
|
+
purpose: profile?.purpose ?? null,
|
|
157
|
+
appliesTo: profile?.appliesTo ?? null,
|
|
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,
|
|
172
|
+
verification: profile?.verification ?? { layers: [] },
|
|
173
|
+
problems,
|
|
174
|
+
status: effectiveStatus,
|
|
175
|
+
nextAction: problems.length
|
|
176
|
+
? status === 'unconfigured'
|
|
177
|
+
? 'Declare application.shape and application.stacks, then run lighthouse profiles --format json.'
|
|
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.'
|
|
181
|
+
: null,
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
const status = selected.length === 0
|
|
185
|
+
? 'not-applicable'
|
|
186
|
+
: selected.some((profile) => profile.status === 'failed')
|
|
187
|
+
? 'failed'
|
|
188
|
+
: selected.some((profile) => profile.status === 'unconfigured')
|
|
189
|
+
? 'unconfigured'
|
|
190
|
+
: 'passed';
|
|
191
|
+
return { schemaVersion: 1, application, selected, available: catalog.profiles.map((profile) => ({ ...profile })), status };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function profileCompositionResult(composition) {
|
|
195
|
+
return {
|
|
196
|
+
id: 'foundation.profile-composition',
|
|
197
|
+
owner: '@juangcarmona/lighthouse',
|
|
198
|
+
level: 'recommended',
|
|
199
|
+
kind: 'hard',
|
|
200
|
+
verification: 'deterministic',
|
|
201
|
+
status: composition.status,
|
|
202
|
+
evidence: composition.selected.map((profile) => ({ id: profile.id, status: profile.status, layers: profile.verification.layers.map((layer) => layer.id) })),
|
|
203
|
+
problems: composition.selected.flatMap((profile) => profile.problems),
|
|
204
|
+
nextAction: composition.selected.find((profile) => profile.nextAction)?.nextAction ?? null,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { readdir, readFile, mkdir, mkdtemp, writeFile, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
const root = process.cwd();
|
|
7
|
+
const nestedSolution = resolve(root, 'src/dotnet/Lighthouse.slnx');
|
|
8
|
+
const rootSolutions = (await readdir(root)).filter((name) => name.endsWith('.slnx'));
|
|
9
|
+
const solutionPath = await readFile(nestedSolution).then(() => nestedSolution).catch(() => {
|
|
10
|
+
if (rootSolutions.length !== 1) throw new Error('Expected src/dotnet/Lighthouse.slnx or one generated root solution.');
|
|
11
|
+
return resolve(root, rootSolutions[0]);
|
|
12
|
+
});
|
|
13
|
+
const dotnetRoot = dirname(solutionPath);
|
|
14
|
+
const lighthouseSourceRoot = solutionPath === nestedSolution ? dotnetRoot : resolve(root, 'src/dotnet');
|
|
15
|
+
const solutionName = solutionPath.slice(dotnetRoot.length + 1);
|
|
16
|
+
function run(args, options = {}) {
|
|
17
|
+
const result = spawnSync('dotnet', args, { cwd: dotnetRoot, encoding: 'utf8', ...options });
|
|
18
|
+
if (result.error) throw result.error;
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
for (const args of [
|
|
22
|
+
['restore', solutionName, '--locked-mode'],
|
|
23
|
+
['build', solutionName, '--no-restore', '-c', 'Release'],
|
|
24
|
+
['test', solutionName, '--no-build', '--no-restore', '-c', 'Release']
|
|
25
|
+
]) {
|
|
26
|
+
const result = run(args, { stdio: 'inherit' });
|
|
27
|
+
if (result.status !== 0) process.exit(result.status ?? 1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const sourceFiles = [];
|
|
31
|
+
async function collectSource(folder) {
|
|
32
|
+
for (const entry of await readdir(resolve(dotnetRoot, folder), { withFileTypes: true })) {
|
|
33
|
+
if (['bin', 'obj', 'node_modules'].includes(entry.name)) continue;
|
|
34
|
+
const path = folder === '.' ? entry.name : `${folder}/${entry.name}`;
|
|
35
|
+
if (entry.isDirectory()) await collectSource(path);
|
|
36
|
+
else if (entry.isFile() && entry.name.endsWith('.cs')) sourceFiles.push(path);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
await collectSource('.');
|
|
40
|
+
const formatted = run(['format', 'whitespace', '.', '--folder', '--verify-no-changes',
|
|
41
|
+
'--include', ...sourceFiles, '--verbosity', 'quiet'], { stdio: 'inherit' });
|
|
42
|
+
if (formatted.status !== 0) process.exit(formatted.status ?? 1);
|
|
43
|
+
|
|
44
|
+
const solution = await readFile(solutionPath, 'utf8');
|
|
45
|
+
for (const match of solution.matchAll(/<Project Path="([^"]+\.csproj)"/g)) {
|
|
46
|
+
const result = run(['msbuild', resolve(dotnetRoot, match[1]), '-getProperty:AssemblyName,LighthouseModule', '-getItem:ProjectReference']);
|
|
47
|
+
if (result.status !== 0) throw new Error(`Cannot evaluate project policy: ${match[1]}`);
|
|
48
|
+
const evaluated = JSON.parse(result.stdout);
|
|
49
|
+
const assembly = evaluated.Properties.AssemblyName;
|
|
50
|
+
if (!assembly.includes('.Modules.') || assembly.endsWith('.Contracts') || assembly.endsWith('Tests')) continue;
|
|
51
|
+
const active = evaluated.Properties.LighthouseModule === 'true';
|
|
52
|
+
const attached = evaluated.Items.ProjectReference.some((item) => item.Identity.endsWith('Lighthouse.Analyzers.csproj')
|
|
53
|
+
&& item.OutputItemType === 'Analyzer' && item.ReferenceOutputAssembly === 'false');
|
|
54
|
+
if (!active || !attached) throw new Error(`Module ${assembly} must activate and reference the Lighthouse analyzer.`);
|
|
55
|
+
}
|
|
56
|
+
process.stdout.write('Every implementation module in the solution activates the analyzer.\n');
|
|
57
|
+
|
|
58
|
+
const probe = await mkdtemp(join(tmpdir(), 'lighthouse-rejection-'));
|
|
59
|
+
const xml = (value) => value.replaceAll('&', '&').replaceAll('"', '"').replaceAll('<', '<');
|
|
60
|
+
try {
|
|
61
|
+
await mkdir(resolve(probe, 'Sibling'));
|
|
62
|
+
await mkdir(resolve(probe, 'Invalid'));
|
|
63
|
+
await writeFile(resolve(probe, 'Sibling/Sibling.csproj'), '<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFramework>net10.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings><AssemblyName>JuanGCarmona.Probe.Modules.Sibling</AssemblyName></PropertyGroup></Project>');
|
|
64
|
+
await writeFile(resolve(probe, 'Sibling/Sibling.cs'), 'namespace JuanGCarmona.Probe.Modules.Sibling; public sealed class SiblingExport {}');
|
|
65
|
+
await writeFile(resolve(probe, 'Invalid/Invalid.csproj'), `<Project Sdk="Microsoft.NET.Sdk">
|
|
66
|
+
<PropertyGroup><TargetFramework>net10.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings><AssemblyName>JuanGCarmona.Probe.Modules.Invalid</AssemblyName><LighthouseModule>true</LighthouseModule></PropertyGroup>
|
|
67
|
+
<ItemGroup>
|
|
68
|
+
<CompilerVisibleProperty Include="LighthouseModule" />
|
|
69
|
+
<ProjectReference Include="../Sibling/Sibling.csproj" />
|
|
70
|
+
<ProjectReference Include="${xml(resolve(lighthouseSourceRoot, 'Lighthouse.Cqrs/Lighthouse.Cqrs.csproj'))}" />
|
|
71
|
+
<ProjectReference Include="${xml(resolve(lighthouseSourceRoot, 'Lighthouse.Analyzers/Lighthouse.Analyzers.csproj'))}" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
|
|
72
|
+
</ItemGroup></Project>`);
|
|
73
|
+
await writeFile(resolve(probe, 'Invalid/Invalid.cs'), `using JuanGCarmona.Lighthouse.Cqrs;
|
|
74
|
+
namespace JuanGCarmona.Probe.Modules.Invalid;
|
|
75
|
+
public class ExposedHandler : ICommandHandler<string, string> {
|
|
76
|
+
public Task<string> Handle(string request, CancellationToken token) => Task.FromResult(request);
|
|
77
|
+
}`);
|
|
78
|
+
const result = run(['build', resolve(probe, 'Invalid/Invalid.csproj'), '-c', 'Release']);
|
|
79
|
+
const output = result.stdout + result.stderr;
|
|
80
|
+
if (result.status === 0 || !/error LH1001\b/.test(output) || !/error LH1002\b/.test(output)) {
|
|
81
|
+
process.stderr.write(output);
|
|
82
|
+
throw new Error('The real build must reject both the exposed handler and sibling implementation reference.');
|
|
83
|
+
}
|
|
84
|
+
process.stdout.write('Build rejection rehearsal passed: LH1001 and LH1002 blocked the invalid module.\n');
|
|
85
|
+
} finally { await rm(probe, { recursive: true, force: true }); }
|