feat: add self-operated analytics and enforce Lastleg module boundaries
This commit is contained in:
@@ -13,6 +13,7 @@ const sourceRoots = {
|
||||
const internalPrefix = 'com.baiyue.lastleg.';
|
||||
|
||||
const sliceRules = [
|
||||
['selfoperated-analytics', /^com\.baiyue\.lastleg\.selfoperated\.analytics(?:\.|$)/],
|
||||
['fleet', /^com\.baiyue\.lastleg\.fleet(?:\.|$)/],
|
||||
['security-projection', /(?:^|\.)security\.(?:controller|dto|service)(?:\.|$)/],
|
||||
['dispatch-delivery', /(?:^|\.)(?:dispatch|delivery|outbound)(?:\.|$)/],
|
||||
|
||||
@@ -10,6 +10,27 @@ const analyzer = path.join(moduleRoot, 'scripts', 'analyze-lastleg-dependencies.
|
||||
const contract = path.join(moduleRoot, 'src', 'test', 'resources', 'architecture', 'lastleg-slice-dependencies.json');
|
||||
const vehiclePrefix = 'com.baiyue.lastleg.masterdata.vehicle';
|
||||
|
||||
test('selfoperated analytics is independent of customer outbound and inventory slices', () => {
|
||||
const report = analyze();
|
||||
const prefix = 'com.baiyue.lastleg.selfoperated.analytics.';
|
||||
const classes = report.classes.filter(entry => entry.fqcn.startsWith(prefix));
|
||||
assert.ok(classes.length > 0);
|
||||
assert.ok(classes.some(entry => entry.packageName.endsWith('.inventory')));
|
||||
assert.ok(classes.some(entry => entry.packageName.endsWith('.outbound')));
|
||||
assert.ok(classes.every(entry => entry.slice === 'selfoperated-analytics'));
|
||||
assert.deepEqual(report.sliceEdges.filter(edge => edge.source === 'selfoperated-analytics'
|
||||
|| edge.target === 'selfoperated-analytics'), [{ source: 'selfoperated-analytics', target: 'shared-technical', count: 1 }]);
|
||||
assert.deepEqual(report.classDependencies.filter(edge => edge.source.startsWith(prefix) && !edge.target.startsWith(prefix)), [{
|
||||
source: prefix + 'request.SelfopEgressController', target: 'com.baiyue.lastleg.security.InternalServiceTokenGuard',
|
||||
}]);
|
||||
assert.deepEqual(report.packageSccs.filter(cycle => cycle.some(pkg => pkg.startsWith(prefix))), []);
|
||||
const baseline = JSON.parse(readFileSync(contract, 'utf8'));
|
||||
const source = classes.find(entry => entry.packageName.endsWith('.inventory')).fqcn;
|
||||
const target = 'com.baiyue.lastleg.inventory.query.service.InventoryQueryService';
|
||||
report.classDependencies.push({ source, target });
|
||||
assert.ok(contractViolations(report, baseline).some(message => message.includes(`${source}->${target}`)));
|
||||
});
|
||||
|
||||
test('fleet stays a business slice and its policies have no infrastructure dependencies', () => {
|
||||
const report = analyze();
|
||||
const classes = report.classes.filter(entry => entry.fqcn.startsWith('com.baiyue.lastleg.fleet.'));
|
||||
@@ -30,6 +51,10 @@ test('fleet stays a business slice and its policies have no infrastructure depen
|
||||
&& edge.target.startsWith('com.baiyue.lastleg.fleet.'));
|
||||
assert.ok(dispatchEdges.length > 0);
|
||||
assert.ok(dispatchEdges.every(edge => /\.(FleetDispatchGateway|FleetDispatchBindingGuard|FleetTripExecutionService|FleetExecutionModels|FleetCompletionModels)$/.test(edge.target)
|
||||
|| edge.source === 'com.baiyue.lastleg.dispatch.fleet.FleetDispatchPlanningAdapter'
|
||||
&& /\.(FleetBookingIncomingGateway|FleetDispatchPlanningGateway)$/.test(edge.target)
|
||||
|| edge.source === 'com.baiyue.lastleg.dispatch.fleet.FleetDispatchPlanningMapper'
|
||||
&& edge.target === 'com.baiyue.lastleg.fleet.trip.FleetDispatchPlanningGateway'
|
||||
|| edge.source === 'com.baiyue.lastleg.dispatch.fleet.PdaFleetHandoverController'
|
||||
&& edge.target === 'com.baiyue.lastleg.fleet.exception.FleetHandoverPdaQuery'),
|
||||
'dispatch must use fleet ports and guards, not its persistence mappers');
|
||||
@@ -42,6 +67,7 @@ function analyze(sourceSet = 'main') {
|
||||
return JSON.parse(execFileSync(process.execPath, [analyzer, '--source-set', sourceSet, '--format', 'json'], {
|
||||
cwd: moduleRoot,
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 8 * 1024 * 1024,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -85,7 +111,10 @@ test('vehicle master data remains the reviewed low-coupling first-slice candidat
|
||||
target: 'com.baiyue.lastleg.security.OverseasPermissions',
|
||||
}]);
|
||||
assert.ok(incoming.length > 0);
|
||||
assert.ok(incoming.every(edge => edge.source.startsWith('com.baiyue.lastleg.fleet.') && edge.target.startsWith(vehiclePrefix + '.application.')));
|
||||
assert.ok(incoming.every(edge => edge.source.startsWith('com.baiyue.lastleg.fleet.') && edge.target.startsWith(vehiclePrefix + '.application.')
|
||||
|| edge.source === 'com.baiyue.lastleg.export.LastlegExportProviderService'
|
||||
&& [vehiclePrefix + '.application.VehicleApplicationService', vehiclePrefix + '.application.model.VehicleModels'].includes(edge.target)),
|
||||
'vehicle consumers use application contracts; the export composition has two exact read-only references');
|
||||
assert.equal(report.packageSccs.some(component => component.some(name => name.startsWith(vehiclePrefix))), false);
|
||||
assert.equal(classes.some(entry => entry.kind === 'cross-service-adapter'), false);
|
||||
assert.equal(classes.some(entry => entry.imports.some(imported => /RestClient|\.client\./.test(imported))), false);
|
||||
@@ -126,6 +155,22 @@ test('reviewed references do not allow another consumer into the same slice', ()
|
||||
assert.ok(contractViolations(report, baseline).some(message => message.includes(`${source}->${target}`)));
|
||||
});
|
||||
|
||||
test('export composition approvals do not grant persistence access or new consumers', () => {
|
||||
const baseline = JSON.parse(readFileSync(contract, 'utf8'));
|
||||
const source = 'com.baiyue.lastleg.export.LastlegExportProviderService';
|
||||
const cases = [
|
||||
{ source, target: vehiclePrefix + '.adapter.out.persistence.VehicleMapper' },
|
||||
{ source, target: 'com.baiyue.lastleg.masterdata.exchange.mapper.MonthlyExchangeRateMapper' },
|
||||
{ source: 'com.baiyue.lastleg.export.LastlegExportColumns', target: 'com.baiyue.lastleg.fleet.quote.FleetQuoteService' },
|
||||
];
|
||||
for (const edge of cases) {
|
||||
const report = analyze();
|
||||
report.classDependencies.push(edge);
|
||||
assert.ok(contractViolations(report, baseline).some(message => message.includes(`${edge.source}->${edge.target}`)),
|
||||
'each unreviewed consumer/target pair must still fail');
|
||||
}
|
||||
});
|
||||
|
||||
test('restoring a retired projection or importing its missing type still fails', () => {
|
||||
const baseline = JSON.parse(readFileSync(contract, 'utf8'));
|
||||
const report = analyze();
|
||||
|
||||
Reference in New Issue
Block a user