build(server): lint:service-http counts envelopes in every domain file and every spelling of a branch on one

The ratchet only read *.service.ts for status literals and one branch shape in controllers and tools, so an envelope in a helper, impl or provider file, a branch written as result.error === 'not_found', and RPC handlers all went uncounted. It now counts the literals in every file under src/nest outside the HTTP layer, and comparisons, negations, optional chains and ternaries on a returned error in controllers, tools and RPC handlers. The baseline takes what the wider count finds.
This commit is contained in:
Maurice
2026-10-10 19:50:42 +02:00
parent ebd8a4f8c9
commit 8fb888c8a3
4 changed files with 84 additions and 20 deletions
+13 -3
View File
@@ -5,10 +5,15 @@
"src/nest/auth/passkey.controller.ts": 1,
"src/nest/auth/passkey.service.ts": 2,
"src/nest/auth/registration-invites.service.ts": 1,
"src/nest/collab/collab.controller.ts": 1,
"src/nest/backup/backup-archive.ts": 8,
"src/nest/backup/backup.impl.ts": 1,
"src/nest/collab/collab.controller.ts": 7,
"src/nest/collab/collab.mcp.ts": 3,
"src/nest/collab/collab.rpc.ts": 2,
"src/nest/doc-sync/doc-sync-config.service.ts": 2,
"src/nest/llm-parse/llm-local.service.ts": 7,
"src/nest/maps/maps-url.resolver.ts": 2,
"src/nest/maps/providers/osm.client.ts": 4,
"src/nest/memories/immich.controller.ts": 6,
"src/nest/memories/immich.service.ts": 15,
"src/nest/memories/memories.mcp.ts": 6,
@@ -17,17 +22,22 @@
"src/nest/memories/unified.controller.ts": 7,
"src/nest/notifications/push/push.controller.ts": 1,
"src/nest/oauth/oauth-api.controller.ts": 4,
"src/nest/oauth/oauth-public.controller.ts": 1,
"src/nest/oauth/oauth-public.controller.ts": 3,
"src/nest/oauth/oauth.service.ts": 22,
"src/nest/oidc/oidc.controller.ts": 3,
"src/nest/oidc/oidc.controller.ts": 4,
"src/nest/oidc/oidc.service.ts": 1,
"src/nest/photos/photos.controller.ts": 1,
"src/nest/place-import/place-import.service.ts": 1,
"src/nest/place-import/providers/google-directions.provider.ts": 5,
"src/nest/place-import/providers/google-list.provider.ts": 12,
"src/nest/place-import/providers/naver-list.provider.ts": 10,
"src/nest/plugins/contributions/plugin-pois.controller.ts": 1,
"src/nest/plugins/contributions/plugin-pois.mcp.ts": 1,
"src/nest/plugins/contributions/plugin-pois.service.ts": 4,
"src/nest/public-api/public-api-request.ts": 3,
"src/nest/reservations/reservations.mcp.ts": 4,
"src/nest/tokens/token.service.ts": 2,
"src/nest/vacay/vacay.controller.ts": 1,
"src/nest/vacay/vacay.mcp.ts": 1
}
}
+36 -11
View File
@@ -7,11 +7,15 @@
* into REST's `{ error }` and the MCP registry into the tool's errorResult, so no
* caller translates it. What this counts is the older way, where every caller did:
*
* - in a *.service.ts: a `status: 4xx` or `status: 5xx` literal (the
* `return { error, status }` result) and `new HttpException(` (HTTP in the
* domain layer);
* - in a *.controller.ts or *.mcp.ts: a branch on a returned error,
* `if (result.error)`, `if ('error' in result)`.
* - in any file under src/nest outside the HTTP and adapter layers (services,
* helpers, impl files, providers): a `status: 4xx` or `status: 5xx` literal
* (the `return { error, status }` result) and `new HttpException(` (HTTP in
* the domain layer). Controllers, guards, pipes, filters, interceptors and
* DTOs speak HTTP by design, and src/nest/common/domain-error.ts defines the
* status a refusal carries, so those are left out by name;
* - in a *.controller.ts, *.mcp.ts or *.rpc.ts: a branch on a returned error,
* `if (result.error)`, `if (!r.error)`, `'error' in result`,
* `result.error === 'not_found'`, `r?.error && ...`, `x.error ? a : b`.
*
* Each file is held to its entry in scripts/service-http-baseline.json, and a
* file without an entry holds none (scripts/lib/count-ratchet.mjs has the rules).
@@ -23,21 +27,42 @@ import { cliMain } from './lib/count-ratchet.mjs';
const STATUS_LITERAL = /\bstatus:\s*[45]\d\d\b/g;
const HTTP_EXCEPTION = /\bnew HttpException\(/g;
const RESULT_ERROR_BRANCH =
/\bif\s*\(\s*(?:'error'\s+in\s+[\w$.]+|"error"\s+in\s+[\w$.]+|[\w$]+(?:\.[\w$]+)*\.error\s*\))/g;
// A property path such as result, res.body or this.state?.last.
const PATH = String.raw`[\w$]+(?:\??\.[\w$]+)*`;
const RESULT_ERROR_BRANCH = new RegExp(
[
String.raw`['"]error['"]\s+in\s+[\w$.]+`,
String.raw`\bif\s*\(\s*!?\s*${PATH}\??\.error\s*\)`,
String.raw`${PATH}\??\.error\b\s*(?:===|!==|==|!=|&&|\|\||\?(?![.?]))`,
].join('|'),
'g',
);
/** Files that speak HTTP by design: the status literals in them are not an envelope. */
const HTTP_LAYER = /\.(?:controller|guard|pipe|filter|interceptor|dto)\.ts$/;
const DEFINES_THE_STATUS = new Set(['src/nest/common/domain-error.ts']);
/** The adapters over a service, where a branch on its returned error is the caller translating it. */
const ADAPTER = /\.(?:controller|mcp|rpc)\.ts$/;
const hits = (text, pattern) => text.match(pattern)?.length ?? 0;
/** The envelope idioms in one service file. */
/** The envelope idioms in one domain-layer file. */
export function countServiceIdioms(text) {
return hits(text, STATUS_LITERAL) + hits(text, HTTP_EXCEPTION);
}
/** The branches on a returned error in one controller or MCP file. */
/** The branches on a returned error in one controller, MCP or RPC file. */
export function countResultBranches(text) {
return hits(text, RESULT_ERROR_BRANCH);
}
/** What one file under src/nest holds, by the layer its name puts it in. */
export function countFile(text, file) {
let n = ADAPTER.test(file) ? countResultBranches(text) : 0;
if (!HTTP_LAYER.test(file) && !DEFINES_THE_STATUS.has(file)) n += countServiceIdioms(text);
return n;
}
export const check = {
name: 'service-http',
script: 'lint:service-http',
@@ -45,8 +70,8 @@ export const check = {
baseline: 'scripts/service-http-baseline.json',
unit: 'error-envelope idiom(s)',
advice: 'Throw a DomainError from the service (src/nest/common/domain-error.ts) and let REST and MCP answer it.',
include: (file) => /\.(service|controller|mcp)\.ts$/.test(file),
count: (text, file) => (file.endsWith('.service.ts') ? countServiceIdioms(text) : countResultBranches(text)),
include: (file) => file.endsWith('.ts') && !file.endsWith('.d.ts'),
count: countFile,
};
cliMain(check, import.meta.url);