build(server): lint:boundaries counts the RPC and MCP layers as domains and sees getRepository calls

src/nest-rpc, src/nest-mcp and src/mcp import domains and are imported by them, but the domain map only knew src/nest, so their edges dropped out of domainCycles and their repository injections out of foreignRepositories. Each layer now counts as one domain. The repository rule reads the syntax tree for @InjectRepository(X) and for getRepository(X) on an EntityManager, so a comment no longer counts and a foreign read through the EntityManager no longer passes. The baseline takes the cycles and the repository reaches the wider view finds; settings -> Users goes, it was a comment.
This commit is contained in:
Maurice
2026-10-10 19:46:25 +02:00
parent 0f6ae8b928
commit ebd8a4f8c9
5 changed files with 230 additions and 16 deletions
+116 -2
View File
@@ -2,27 +2,106 @@
"fileCycles": [],
"domainCycles": [
"accommodations -> assignments",
"accommodations -> nest-rpc",
"accommodations -> places",
"assignments -> days",
"assignments -> journey",
"assignments -> nest-rpc",
"atlas -> nest-rpc",
"auth -> budget",
"auth -> files",
"auth -> plugins",
"budget -> nest-rpc",
"calendar -> days",
"calendar -> reservations",
"categories -> nest-rpc",
"collab -> files",
"collab -> nest-rpc",
"collections -> nest-rpc",
"collections -> places",
"day-notes -> nest-rpc",
"days -> accommodations",
"days -> assignments",
"days -> nest-rpc",
"days -> places",
"files -> nest-rpc",
"geo -> maps",
"integrations -> assignments",
"integrations -> atlas",
"integrations -> journey",
"integrations -> places",
"integrations -> reservations",
"journey -> atlas",
"journey -> files",
"journey -> nest-rpc",
"maps -> geo",
"memories -> photos",
"nest-rpc -> plugins",
"packing -> nest-rpc",
"photos -> memories",
"places -> accommodations",
"places -> assignments",
"places -> atlas",
"places -> journey",
"places -> nest-rpc",
"plugins -> accommodations",
"plugins -> assignments",
"plugins -> atlas",
"plugins -> budget",
"plugins -> categories",
"plugins -> collab",
"plugins -> collections",
"plugins -> day-notes",
"plugins -> days",
"plugins -> files",
"plugins -> journey",
"plugins -> nest-rpc",
"plugins -> packing",
"plugins -> places",
"plugins -> reservations",
"plugins -> tags",
"plugins -> todo",
"plugins -> trips",
"plugins -> vacay",
"plugins -> weather",
"reservations -> accommodations",
"reservations -> assignments",
"reservations -> budget",
"reservations -> days",
"reservations -> integrations",
"reservations -> nest-rpc",
"tags -> nest-rpc",
"todo -> nest-rpc",
"trip-members -> auth",
"trip-members -> budget",
"trip-members -> trips",
"trip-read-model -> accommodations",
"trip-read-model -> budget",
"trip-read-model -> collab",
"trip-read-model -> days",
"trip-read-model -> files",
"trip-read-model -> packing",
"trip-read-model -> places",
"trip-read-model -> reservations",
"trip-read-model -> todo",
"trip-read-model -> trip-members",
"trip-read-model -> trips",
"trips -> accommodations",
"trips -> budget",
"trips -> calendar",
"trips -> collab",
"trips -> days",
"trips -> files",
"trips -> nest-rpc",
"trips -> packing",
"trips -> places",
"trips -> reservations",
"trips -> todo",
"trips -> trip-members",
"trips -> trip-read-model"
"trips -> trip-read-model",
"trips -> vacay",
"vacay -> nest-rpc",
"weather -> nest-rpc"
],
"sharedImportsDomain": [
"nest/common/idempotency-cleanup.job.ts -> nest/audit/audit-log.logger.ts",
@@ -61,6 +140,7 @@
"journey -> storage/storage-upload.factory.ts",
"maps -> geo/nominatim.client.ts",
"maps -> settings/instance-api-keys.ts",
"nest-rpc -> plugins/protocol/envelope.ts",
"place-enrichment -> maps/maps.helpers.ts",
"place-enrichment -> maps/trek-places.client.ts",
"places -> atlas/atlas-geo.ts",
@@ -144,8 +224,10 @@
"auth -> TripMembers (owned by trip-membership)",
"auth -> Trips (owned by trips)",
"auth-core -> AppSettings (owned by settings)",
"auth-core -> UserSessions (owned by sessions)",
"auth-core -> Users (owned by auth)",
"auth-core -> WebauthnCredentials (owned by auth)",
"backup -> UserSessions (owned by sessions)",
"booking-import -> Days (owned by days)",
"booking-import -> Reservations (owned by reservations)",
"budget -> Places (owned by places)",
@@ -164,6 +246,7 @@
"collections -> TripMembers (owned by trip-membership)",
"collections -> Trips (owned by trips)",
"collections -> Users (owned by auth)",
"common -> Users (owned by auth)",
"day-notes -> Days (owned by days)",
"days -> DayAccommodations (owned by accommodations)",
"days -> DayAssignments (owned by assignments)",
@@ -184,6 +267,7 @@
"files -> DayAssignments (owned by assignments)",
"files -> Places (owned by places)",
"files -> Reservations (owned by reservations)",
"files -> UserSessions (owned by sessions)",
"files -> Users (owned by auth)",
"google-quota -> AppSettings (owned by settings)",
"integrations -> AppSettings (owned by settings)",
@@ -210,6 +294,8 @@
"memories -> TripPhotos (owned by photos)",
"memories -> Trips (owned by trips)",
"memories -> Users (owned by auth)",
"nest-rpc -> Trips (owned by trips)",
"nest-rpc -> Users (owned by auth)",
"notifications -> AppSettings (owned by settings)",
"notifications -> Settings (owned by settings)",
"notifications -> TodoItems (owned by todo)",
@@ -235,6 +321,10 @@
"places -> Tags (owned by tags)",
"places -> TripMembers (owned by trip-membership)",
"places -> Trips (owned by trips)",
"platform -> Photos (owned by photos)",
"platform -> ShareTokens (owned by share)",
"platform -> UserSessions (owned by sessions)",
"platform -> Users (owned by auth)",
"plugins -> DayAccommodations (owned by accommodations)",
"plugins -> Days (owned by days)",
"plugins -> JourneyEntries (owned by journey)",
@@ -243,6 +333,7 @@
"plugins -> Reservations (owned by reservations)",
"plugins -> Settings (owned by settings)",
"plugins -> Trips (owned by trips)",
"plugins -> UserSessions (owned by sessions)",
"plugins -> Users (owned by auth)",
"public-api -> BucketList (owned by atlas)",
"public-api -> DayNotes (owned by day-notes)",
@@ -274,7 +365,6 @@
"roadtrip -> Users (owned by auth)",
"route-usage -> AppSettings (owned by settings)",
"school-holidays -> VacayHolidayCalendars (owned by vacay)",
"settings -> Users (owned by auth)",
"share -> BudgetItems (owned by budget)",
"share -> Categories (owned by categories)",
"share -> CollabMessages (owned by collab)",
@@ -299,6 +389,30 @@
"trip-members -> Users (owned by auth)",
"trip-membership -> Trips (owned by trips)",
"trip-read-model -> Trips (owned by trips)",
"trips -> AssignmentParticipants (owned by assignments)",
"trips -> BudgetCategoryOrder (owned by budget)",
"trips -> BudgetItemMembers (owned by budget)",
"trips -> BudgetItemPayers (owned by budget)",
"trips -> BudgetItems (owned by budget)",
"trips -> DayAccommodations (owned by accommodations)",
"trips -> DayAssignments (owned by assignments)",
"trips -> DayNotes (owned by day-notes)",
"trips -> Days (owned by days)",
"trips -> JourneyEntries (owned by journey)",
"trips -> PackingBags (owned by packing)",
"trips -> PackingItems (owned by packing)",
"trips -> Places (owned by places)",
"trips -> Reservations (owned by reservations)",
"trips -> RoadtripDayBoundaries (owned by roadtrip)",
"trips -> RoadtripDayTracks (owned by roadtrip)",
"trips -> RoadtripPreferences (owned by roadtrip)",
"trips -> RoadtripVias (owned by roadtrip)",
"trips -> Tags (owned by tags)",
"trips -> TodoItems (owned by todo)",
"trips -> TourWaypoints (owned by tours)",
"trips -> Tours (owned by tours)",
"trips -> TripMembers (owned by trip-membership)",
"trips -> Users (owned by auth)",
"unsplash -> AppSettings (owned by settings)",
"unsplash -> Users (owned by auth)",
"vacay -> SchoolHolidayRegions (owned by school-holidays)"
+60 -13
View File
@@ -16,7 +16,9 @@
* So are the imports of src/db/entities/: MikroORM relations
* point both ways by design and are read through `() => X`
* thunks after every file has loaded.
* domainCycles the same, between src/nest/<domain> folders: domain A
* domainCycles the same, between src/nest/<domain> folders (and the
* layers in LAYERS, src/nest-rpc, src/nest-mcp and src/mcp,
* each counted as one domain): domain A
* imports from B while B (directly or through others)
* imports from A. Keyed by the domain pair, so a further
* import along a dependency that already exists is not new;
@@ -38,8 +40,10 @@
* dbImportsNest a file under src/db/ importing from src/nest/. The data
* layer sits below the domains, never the other way round.
* foreignRepositories
* a class in src/nest/<A>/ injecting (@InjectRepository) the
* repository of an entity another domain owns. Ownership is
* a class in src/nest/<A>/ (or one of LAYERS) reaching the
* repository of an entity another domain owns, through
* @InjectRepository(X) or a getRepository(X) call on an
* EntityManager. Ownership is
* the map in scripts/repository-owners.json (entity class ->
* owning domain folder); an injected entity missing from it
* stops the run. Keyed by domain and entity, so a second
@@ -282,7 +286,22 @@ function cycleEdges(pairs) {
return out;
}
const domainOf = (file) => file.match(/^nest\/([^/]+)\//)?.[1] ?? null;
/**
* Layers outside src/nest that work like a domain of their own: the plugin RPC
* kit, the MCP decorator layer and the process-wide MCP state. They import
* domains and domains import them, so they take part in domainCycles and
* foreignRepositories under their folder name; moving a file out of src/nest
* does not take its edges out of the count.
*/
export const LAYERS = ['nest-rpc', 'nest-mcp', 'mcp'];
/** The domain a src/-relative path belongs to: nest/<domain>/..., one of LAYERS, or null. */
export const domainOf = (file) => {
const nest = file.match(/^nest\/([^/]+)\//)?.[1];
if (nest) return nest;
const top = file.split('/')[0];
return file.includes('/') && LAYERS.includes(top) ? top : null;
};
/** The orchestrators no provider may import: its own domain's main service, and MapsService everywhere. */
function isOrchestratorOf(providerFile, target) {
@@ -293,6 +312,8 @@ function isOrchestratorOf(providerFile, target) {
const isProviderFile = (file) => /^nest\/[^/]+\/(?:.+\/)?providers\//.test(file);
export function isPublic(target) {
// A layer outside src/nest is a toolkit every domain is written against.
if (!target.startsWith('nest/')) return true;
const inner = target.slice('nest/'.length);
const domain = inner.split('/')[0];
if (SHARED_DOMAINS.has(domain)) return true;
@@ -300,18 +321,44 @@ export function isPublic(target) {
return PUBLIC_SUFFIXES.some((s) => target.endsWith(s));
}
/** Every @InjectRepository(Entity) under src/nest: { domain, entity, file }. */
/**
* The entity classes a file reaches a repository of: `@InjectRepository(X)`
* and `<anything>.getRepository(X)`, read from the syntax tree so a comment or
* a string naming them does not count.
*/
export function repositoryEntitiesOf(text, fileName = 'file.ts') {
const source = ts.createSourceFile(fileName, text, ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
const found = [];
const visit = (node) => {
if (ts.isCallExpression(node) && node.arguments.length > 0 && ts.isIdentifier(node.arguments[0])) {
const callee = node.expression;
const name = ts.isIdentifier(callee)
? callee.text
: ts.isPropertyAccessExpression(callee)
? callee.name.text
: null;
if (name === 'InjectRepository' || name === 'getRepository') found.push(node.arguments[0].text);
}
ts.forEachChild(node, visit);
};
visit(source);
return found;
}
/** Every repository a class under src/nest or one of LAYERS reaches: { domain, entity, file }. */
export function collectRepositoryInjections(serverDir) {
const src = join(serverDir, 'src');
const nest = join(src, 'nest');
if (!existsSync(nest)) return [];
const found = [];
for (const file of walk(nest)) {
const rel = relative(src, file).split(sep).join('/');
const domain = domainOf(rel);
if (!domain) continue;
for (const m of readFileSync(file, 'utf8').matchAll(/@InjectRepository\(\s*([A-Za-z_$][\w$]*)\s*\)/g)) {
found.push({ domain, entity: m[1], file: rel });
for (const top of ['nest', ...LAYERS]) {
const dir = join(src, top);
if (!existsSync(dir)) continue;
for (const file of walk(dir)) {
const rel = relative(src, file).split(sep).join('/');
const domain = domainOf(rel);
if (!domain) continue;
for (const entity of repositoryEntitiesOf(readFileSync(file, 'utf8'), file)) {
found.push({ domain, entity, file: rel });
}
}
}
return found;
+3
View File
@@ -62,6 +62,7 @@
"PasswordResetTokens": "auth",
"PhotoProviderFields": "addons",
"PhotoProviders": "addons",
"Photos": "photos",
"PlaceDetailsCache": "maps",
"PlaceRatings": "places",
"PlaceRegions": "atlas",
@@ -89,9 +90,11 @@
"Reservations": "reservations",
"RoadtripDayBoundaries": "roadtrip",
"RoadtripDayTracks": "roadtrip",
"RoadtripPreferences": "roadtrip",
"RoadtripPreferencesEntity": "roadtrip",
"RoadtripVias": "roadtrip",
"RouteUsageDaily": "route-usage",
"SchedulerLeases": "scheduling",
"SchoolHolidayCountries": "school-holidays",
"SchoolHolidayPeriods": "school-holidays",
"SchoolHolidayRegions": "school-holidays",