@webpieces/nx-webpieces-rules 0.4.809 → 0.4.810
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/package.json +7 -7
- package/src/lib/api-usage/api-doc-rules-scan.d.ts +22 -14
- package/src/lib/api-usage/api-doc-rules-scan.js +32 -21
- package/src/lib/api-usage/api-doc-rules-scan.js.map +1 -1
- package/src/lib/api-usage/api-doc-rules-verdicts.d.ts +14 -1
- package/src/lib/api-usage/api-doc-rules-verdicts.js +19 -4
- package/src/lib/api-usage/api-doc-rules-verdicts.js.map +1 -1
- package/src/lib/api-usage/api-doc-rules.d.ts +42 -5
- package/src/lib/api-usage/api-doc-rules.js +65 -9
- package/src/lib/api-usage/api-doc-rules.js.map +1 -1
- package/src/lib/api-usage/api-scanner.d.ts +5 -5
- package/src/lib/api-usage/api-scanner.js +2 -2
- package/src/lib/api-usage/api-scanner.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/nx-webpieces-rules",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.810",
|
|
4
4
|
"description": "Nx-specific webpieces validation rules and graph tooling. Bundles all @webpieces rule packages with Nx graph validators and an inference plugin.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
"README.md"
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@webpieces/ai-hook-rules": "0.4.
|
|
22
|
-
"@webpieces/api-doc-model": "0.4.
|
|
23
|
-
"@webpieces/code-rules": "0.4.
|
|
24
|
-
"@webpieces/eslint-rules": "0.4.
|
|
25
|
-
"@webpieces/pr-gate": "0.4.
|
|
26
|
-
"@webpieces/rules-config": "0.4.
|
|
21
|
+
"@webpieces/ai-hook-rules": "0.4.810",
|
|
22
|
+
"@webpieces/api-doc-model": "0.4.810",
|
|
23
|
+
"@webpieces/code-rules": "0.4.810",
|
|
24
|
+
"@webpieces/eslint-rules": "0.4.810",
|
|
25
|
+
"@webpieces/pr-gate": "0.4.810",
|
|
26
|
+
"@webpieces/rules-config": "0.4.810",
|
|
27
27
|
"madge": "8.0.0"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `api-rules-for-openapi` and `api-rules-for-mcp` (#1011) — the CI half of "is this contract
|
|
3
|
-
* publishable", on
|
|
3
|
+
* publishable", on every `@ApiPath` class IN SCOPE, `@ApiType` or not.
|
|
4
|
+
*
|
|
5
|
+
* "In scope" is the rule's `mode` (#1017): `AFFECTED_PROJECT` scans the contracts of the projects the
|
|
6
|
+
* diff touched — the granularity nx already builds at, and the mode a consumer normally picks —
|
|
7
|
+
* while `RUN_EVERY_TIME` scans the whole workspace for a migration sweep. `ApiDocRule.coversProject`
|
|
8
|
+
* is the one place that answers it, for both rules.
|
|
4
9
|
*
|
|
5
10
|
* ## The acceptance contract, and why this file drives the generator instead of copying it
|
|
6
11
|
*
|
|
@@ -20,17 +25,19 @@
|
|
|
20
25
|
* field). The extractor maps it to a primitive and the generator publishes `{}`, which in JSON
|
|
21
26
|
* Schema means "anything" — a partner-facing field with no shape, which is the defect the
|
|
22
27
|
* unmapped guard exists for, arriving through a door the guard does not watch.
|
|
23
|
-
* - an
|
|
24
|
-
* endpoint and is allowed there; an
|
|
25
|
-
* breaking change, where a named empty response object
|
|
28
|
+
* - an `rpc` or `external` endpoint whose response is `void`. Fire-and-forget is the CONTRACT of a
|
|
29
|
+
* `cloudtasks` or `cron` endpoint and is allowed there; an endpoint somebody WAITS on that answers
|
|
30
|
+
* nothing can never gain a field without a breaking change, where a named empty response object
|
|
31
|
+
* grows additively forever (#1017 — #1016 read this narrowly as rpc-only because `external` was
|
|
32
|
+
* unstated).
|
|
26
33
|
*
|
|
27
34
|
* ## Why it lives in the rules engine and not in the doc parser
|
|
28
35
|
*
|
|
29
36
|
* `@webpieces/api-doc-model` is only ever pointed at contracts somebody chose to publish. `@ApiType`
|
|
30
37
|
* is a PUBLISHING decision added later, on purpose — so a shape rule that only ran on contracts which
|
|
31
38
|
* had already opted in would let a team discover, six months afterwards, that the type was never
|
|
32
|
-
* expressible, by which time it is in partners' generated clients.
|
|
33
|
-
* contract in
|
|
39
|
+
* expressible, by which time it is in partners' generated clients. So every check below runs on every
|
|
40
|
+
* contract in scope, opted in or not — `@ApiType` narrows nothing here.
|
|
34
41
|
*
|
|
35
42
|
* ## Root-level unions are NOT re-checked here
|
|
36
43
|
*
|
|
@@ -54,7 +61,7 @@ import { ApiDocRule, ApiDocRulesFindings } from './api-doc-rules';
|
|
|
54
61
|
export declare class ApiDocRulesScan {
|
|
55
62
|
private readonly workspaceRoot;
|
|
56
63
|
private readonly projectInfos;
|
|
57
|
-
/** OFF unless a caller read otherwise out of webpieces.config.json
|
|
64
|
+
/** OFF unless a caller read otherwise out of webpieces.config.json, which MUST state it. */
|
|
58
65
|
private readonly openApiRule;
|
|
59
66
|
private readonly mcpRule;
|
|
60
67
|
/**
|
|
@@ -66,7 +73,7 @@ export declare class ApiDocRulesScan {
|
|
|
66
73
|
*/
|
|
67
74
|
private readonly exclusions;
|
|
68
75
|
constructor(workspaceRoot: string, projectInfos: Map<string, ProjectInfo>,
|
|
69
|
-
/** OFF unless a caller read otherwise out of webpieces.config.json
|
|
76
|
+
/** OFF unless a caller read otherwise out of webpieces.config.json, which MUST state it. */
|
|
70
77
|
openApiRule?: ApiDocRule, mcpRule?: ApiDocRule);
|
|
71
78
|
run(): ApiDocRulesFindings;
|
|
72
79
|
/** Every contract in one file, or the ONE refusal that stopped the file being read at all. */
|
|
@@ -84,15 +91,16 @@ export declare class ApiDocRulesScan {
|
|
|
84
91
|
/** `Record<string, unknown>`, `unknown[]`, `x: unknown` — by VALUE TYPE, never by spelling. */
|
|
85
92
|
private judgeUnknownValues;
|
|
86
93
|
/**
|
|
87
|
-
* An
|
|
94
|
+
* An endpoint somebody WAITS ON must NAME a response DTO, even an empty one.
|
|
88
95
|
*
|
|
89
96
|
* `Promise<void>` on a `cloudtasks` or `cron` endpoint is the CONTRACT — fire-and-forget, nothing
|
|
90
|
-
* to shape — and is allowed. On an
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
97
|
+
* to shape — and is allowed. On an `rpc` OR an `external` it is a one-way door: both are
|
|
98
|
+
* synchronous request/response, an outside caller reads what comes back, and a `void` response
|
|
99
|
+
* can never gain a field without breaking every generated client where `{}` grows additively
|
|
100
|
+
* forever. This is a contract-EVOLUTION rule, which is why it is here and not in the MCP half: it
|
|
101
|
+
* is worth having on an endpoint that never becomes a tool. See ANSWERING_KINDS (#1017).
|
|
94
102
|
*/
|
|
95
|
-
private
|
|
103
|
+
private judgeAnsweringResponses;
|
|
96
104
|
/** Everything `McpToolRegistry` refuses to boot on, plus whatever the renderer cannot render. */
|
|
97
105
|
private judgeTools;
|
|
98
106
|
/** Every non-test `.ts` under a project's `src` whose text DECLARES an `@ApiPath` contract. */
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* `api-rules-for-openapi` and `api-rules-for-mcp` (#1011) — the CI half of "is this contract
|
|
4
|
-
* publishable", on
|
|
4
|
+
* publishable", on every `@ApiPath` class IN SCOPE, `@ApiType` or not.
|
|
5
|
+
*
|
|
6
|
+
* "In scope" is the rule's `mode` (#1017): `AFFECTED_PROJECT` scans the contracts of the projects the
|
|
7
|
+
* diff touched — the granularity nx already builds at, and the mode a consumer normally picks —
|
|
8
|
+
* while `RUN_EVERY_TIME` scans the whole workspace for a migration sweep. `ApiDocRule.coversProject`
|
|
9
|
+
* is the one place that answers it, for both rules.
|
|
5
10
|
*
|
|
6
11
|
* ## The acceptance contract, and why this file drives the generator instead of copying it
|
|
7
12
|
*
|
|
@@ -21,17 +26,19 @@
|
|
|
21
26
|
* field). The extractor maps it to a primitive and the generator publishes `{}`, which in JSON
|
|
22
27
|
* Schema means "anything" — a partner-facing field with no shape, which is the defect the
|
|
23
28
|
* unmapped guard exists for, arriving through a door the guard does not watch.
|
|
24
|
-
* - an
|
|
25
|
-
* endpoint and is allowed there; an
|
|
26
|
-
* breaking change, where a named empty response object
|
|
29
|
+
* - an `rpc` or `external` endpoint whose response is `void`. Fire-and-forget is the CONTRACT of a
|
|
30
|
+
* `cloudtasks` or `cron` endpoint and is allowed there; an endpoint somebody WAITS on that answers
|
|
31
|
+
* nothing can never gain a field without a breaking change, where a named empty response object
|
|
32
|
+
* grows additively forever (#1017 — #1016 read this narrowly as rpc-only because `external` was
|
|
33
|
+
* unstated).
|
|
27
34
|
*
|
|
28
35
|
* ## Why it lives in the rules engine and not in the doc parser
|
|
29
36
|
*
|
|
30
37
|
* `@webpieces/api-doc-model` is only ever pointed at contracts somebody chose to publish. `@ApiType`
|
|
31
38
|
* is a PUBLISHING decision added later, on purpose — so a shape rule that only ran on contracts which
|
|
32
39
|
* had already opted in would let a team discover, six months afterwards, that the type was never
|
|
33
|
-
* expressible, by which time it is in partners' generated clients.
|
|
34
|
-
* contract in
|
|
40
|
+
* expressible, by which time it is in partners' generated clients. So every check below runs on every
|
|
41
|
+
* contract in scope, opted in or not — `@ApiType` narrows nothing here.
|
|
35
42
|
*
|
|
36
43
|
* ## Root-level unions are NOT re-checked here
|
|
37
44
|
*
|
|
@@ -48,7 +55,6 @@ const fs = tslib_1.__importStar(require("fs"));
|
|
|
48
55
|
const path = tslib_1.__importStar(require("path"));
|
|
49
56
|
const ts = tslib_1.__importStar(require("typescript"));
|
|
50
57
|
const api_doc_model_1 = require("@webpieces/api-doc-model");
|
|
51
|
-
const rules_config_1 = require("@webpieces/rules-config");
|
|
52
58
|
const api_ast_1 = require("./api-ast");
|
|
53
59
|
const api_doc_rules_1 = require("./api-doc-rules");
|
|
54
60
|
const api_doc_rules_verdicts_1 = require("./api-doc-rules-verdicts");
|
|
@@ -188,7 +194,7 @@ class ApiDocRulesScan {
|
|
|
188
194
|
*/
|
|
189
195
|
exclusions = [];
|
|
190
196
|
constructor(workspaceRoot, projectInfos,
|
|
191
|
-
/** OFF unless a caller read otherwise out of webpieces.config.json
|
|
197
|
+
/** OFF unless a caller read otherwise out of webpieces.config.json, which MUST state it. */
|
|
192
198
|
openApiRule = api_doc_rules_1.ApiDocRule.off(api_doc_rules_1.OPENAPI_RULE), mcpRule = api_doc_rules_1.ApiDocRule.off(api_doc_rules_1.MCP_RULE)) {
|
|
193
199
|
this.workspaceRoot = workspaceRoot;
|
|
194
200
|
this.projectInfos = projectInfos;
|
|
@@ -245,7 +251,7 @@ class ApiDocRulesScan {
|
|
|
245
251
|
const lines = (0, api_doc_rules_verdicts_1.contractLinesOf)(source, model.contractName);
|
|
246
252
|
this.judgeUnmapped(model, sink, source.fileName);
|
|
247
253
|
this.judgeUnknownValues(model, sink, source.fileName);
|
|
248
|
-
this.
|
|
254
|
+
this.judgeAnsweringResponses(model, source, lines, sink);
|
|
249
255
|
this.judgeTools(model, source, lines, sink, toolNames);
|
|
250
256
|
}
|
|
251
257
|
/** Everything `TypeResolver` could not represent — the generator's OWN verdict, re-worded. */
|
|
@@ -269,23 +275,26 @@ class ApiDocRulesScan {
|
|
|
269
275
|
}
|
|
270
276
|
}
|
|
271
277
|
/**
|
|
272
|
-
* An
|
|
278
|
+
* An endpoint somebody WAITS ON must NAME a response DTO, even an empty one.
|
|
273
279
|
*
|
|
274
280
|
* `Promise<void>` on a `cloudtasks` or `cron` endpoint is the CONTRACT — fire-and-forget, nothing
|
|
275
|
-
* to shape — and is allowed. On an
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
281
|
+
* to shape — and is allowed. On an `rpc` OR an `external` it is a one-way door: both are
|
|
282
|
+
* synchronous request/response, an outside caller reads what comes back, and a `void` response
|
|
283
|
+
* can never gain a field without breaking every generated client where `{}` grows additively
|
|
284
|
+
* forever. This is a contract-EVOLUTION rule, which is why it is here and not in the MCP half: it
|
|
285
|
+
* is worth having on an endpoint that never becomes a tool. See ANSWERING_KINDS (#1017).
|
|
279
286
|
*/
|
|
280
|
-
|
|
287
|
+
judgeAnsweringResponses(model, source, lines, sink) {
|
|
281
288
|
for (const endpoint of model.endpoints) {
|
|
282
|
-
|
|
289
|
+
// `.some` and not `.includes`: the model's `kind` is a widened string, and the list is
|
|
290
|
+
// typed EndpointKind so a kind that stops existing is a compile error here.
|
|
291
|
+
if (!api_doc_rules_verdicts_1.ANSWERING_KINDS.some((kind) => kind === endpoint.kind))
|
|
283
292
|
continue;
|
|
284
293
|
if (endpoint.response !== undefined && !(0, api_doc_rules_verdicts_1.isVoidLike)(endpoint.response))
|
|
285
294
|
continue;
|
|
286
295
|
const site = new Site(source.fileName, lines.lineOf(endpoint.methodName));
|
|
287
|
-
sink.shared(() => new api_doc_rules_1.ApiContractDefect(model.contractName, endpoint.methodName,
|
|
288
|
-
'return type)', site.relativeTo(this.workspaceRoot), api_doc_rules_verdicts_1.VOID_RPC_CURE, model.apiTypes), site);
|
|
296
|
+
sink.shared(() => new api_doc_rules_1.ApiContractDefect(model.contractName, endpoint.methodName, `an ${endpoint.kind} endpoint returns nothing a document can name (void, ` +
|
|
297
|
+
'unknown, or no declared return type)', site.relativeTo(this.workspaceRoot), api_doc_rules_verdicts_1.VOID_RPC_CURE, model.apiTypes), site);
|
|
289
298
|
}
|
|
290
299
|
}
|
|
291
300
|
/** Everything `McpToolRegistry` refuses to boot on, plus whatever the renderer cannot render. */
|
|
@@ -319,9 +328,11 @@ class ApiDocRulesScan {
|
|
|
319
328
|
for (const info of this.projectInfos.values()) {
|
|
320
329
|
if (info.root === '' || info.root === '.')
|
|
321
330
|
continue;
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
331
|
+
// ONE question, asked of the rule itself: `mode` (OFF / AFFECTED_PROJECT /
|
|
332
|
+
// RUN_EVERY_TIME) and `allowedPaths` are both folded into coversProject, so the
|
|
333
|
+
// affected-project narrowing cannot be honoured in one branch and skipped in the other.
|
|
334
|
+
const openApi = this.openApiRule.coversProject(info.root);
|
|
335
|
+
const mcp = this.mcpRule.coversProject(info.root);
|
|
325
336
|
if (!openApi && !mcp)
|
|
326
337
|
continue;
|
|
327
338
|
const srcDir = path.join(path.resolve(this.workspaceRoot, info.root), 'src');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-doc-rules-scan.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-doc-rules-scan.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `api-rules-for-openapi` and `api-rules-for-mcp` (#1011) — the CI half of \"is this contract\n * publishable\", on EVERY `@ApiPath` class in the workspace, `@ApiType` or not.\n *\n * ## The acceptance contract, and why this file drives the generator instead of copying it\n *\n * The rules exist so that ADDING `@ApiType(...)` (and `@WpMcpTool`) to a contract that passes them\n * always works. That is only true if \"expressible\" has exactly ONE definition, so this scan runs the\n * generator's own code — `ApiDocExtractor` / `TypeResolver` from `@webpieces/api-doc-model` for the\n * OpenAPI half, and `McpSchemaRenderer` tool-by-tool for the MCP half. A second implementation of\n * \"what can be published\" would drift from the first on the release that improved either one, and\n * the drift would be silent: the rules would stay green while generation started failing. The two\n * packages ship on the same release train, so the dependency is in lockstep by construction.\n *\n * Two things here are STRICTER than the generator, deliberately, and both are publishing rules\n * rather than expressibility ones (being stricter cannot break the acceptance contract — it can only\n * refuse something that would have generated):\n *\n * - an `unknown` VALUE TYPE anywhere (`Record<string, unknown>`, `unknown[]`, a bare `unknown`\n * field). The extractor maps it to a primitive and the generator publishes `{}`, which in JSON\n * Schema means \"anything\" — a partner-facing field with no shape, which is the defect the\n * unmapped guard exists for, arriving through a door the guard does not watch.\n * - an RPC whose response is `void`. Fire-and-forget is the CONTRACT of a `cloudtasks` or `cron`\n * endpoint and is allowed there; an RPC that answers nothing can never gain a field without a\n * breaking change, where a named empty response object grows additively forever.\n *\n * ## Why it lives in the rules engine and not in the doc parser\n *\n * `@webpieces/api-doc-model` is only ever pointed at contracts somebody chose to publish. `@ApiType`\n * is a PUBLISHING decision added later, on purpose — so a shape rule that only ran on contracts which\n * had already opted in would let a team discover, six months afterwards, that the type was never\n * expressible, by which time it is in partners' generated clients. Every check below runs on every\n * contract in the workspace.\n *\n * ## Root-level unions are NOT re-checked here\n *\n * `no-root-union-api-type` (#1009) already refuses them, workspace-wide, with its own config key and\n * its own per-site hatch. One implementation. The MCP half still reports one when it meets it,\n * because `McpSchemaRenderer` refuses it as its own backstop and this scan reports whatever the\n * renderer says — which is the correct division: the OpenAPI document publishes a root union\n * perfectly well, and only a tool schema cannot carry one.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport * as ts from 'typescript';\nimport {\n ApiDocExtractionError,\n ApiDocExtractor,\n ApiDocModel,\n DocumentedEndpoint,\n McpRenderError,\n McpSchemaRenderer,\n TypeRef,\n UnmappedType,\n} from '@webpieces/api-doc-model';\nimport { matchesAnyGlob } from '@webpieces/rules-config';\nimport { ProjectInfo } from '../project-info';\nimport { collectTsFiles, isTestFile } from './api-ast';\nimport {\n ApiContractDefect,\n ApiDocRule,\n ApiDocRulesFindings,\n ApiRuleFindings,\n DisableComment,\n McpExclusion,\n MCP_RULE,\n OPENAPI_RULE,\n} from './api-doc-rules';\nimport {\n ContractLines,\n RPC_KIND,\n unknownValueCure,\n VOID_RPC_CURE,\n carriesUnknown,\n classifyUnmapped,\n contractLinesOf,\n contractNamesIn,\n isVoidLike,\n toolFailures,\n} from './api-doc-rules-verdicts';\n\n/** `@ApiPath(` at COLUMN ZERO — a docstring that TALKS about a contract declares none. */\nconst DECLARES_CONTRACT = /^@ApiPath\\(/m;\n\n/** ONE contract file, and which of the two rules apply to the project that owns it. */\nclass ContractFile {\n constructor(\n public readonly absPath: string,\n public readonly openApi: boolean,\n public readonly mcp: boolean,\n ) {}\n}\n\n/** Where one declaration sits, already split out of the extractor's `File.ts:LINE:COL` spelling. */\nclass Site {\n constructor(\n public readonly absPath: string,\n public readonly line: number,\n ) {}\n\n /** `path/to/File.ts:LINE`, workspace-relative — what a refusal prints. */\n relativeTo(workspaceRoot: string): string {\n return `${path.relative(workspaceRoot, this.absPath)}:${this.line}`;\n }\n\n /** `abs/File.ts:12:5` -> a Site. An unparseable one falls back to line 1 of `fallback`. */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static parse(location: string, fallback: string): Site {\n const match = location.match(/^(.*):(\\d+):\\d+$/);\n if (match === null) return new Site(fallback, 1);\n return new Site(match[1], Number(match[2]));\n }\n}\n\n/**\n * ONE rule's accumulator. It is what applies the per-site disable, so the disable semantics live in\n * exactly one place and cannot differ between the two rules.\n */\nclass DefectCollector {\n private readonly violations: ApiContractDefect[] = [];\n private readonly reasonless: ApiContractDefect[] = [];\n\n constructor(private readonly ruleName: string) {}\n\n /** The rule this collector reports under — what a shared defect's cure has to name. */\n rule(): string {\n return this.ruleName;\n }\n\n add(defect: ApiContractDefect, site: Site): void {\n const disable = DisableComment.readAt(site.absPath, site.line, this.ruleName);\n if (disable === undefined) {\n this.violations.push(defect);\n return;\n }\n if (!disable.hasReason) this.reasonless.push(defect);\n }\n\n findings(): ApiRuleFindings {\n return new ApiRuleFindings(\n DefectCollector.externalFirst(this.violations),\n DefectCollector.externalFirst(this.reasonless),\n );\n }\n\n /**\n * Partner-facing contracts first. The same defect is a different size depending on who reads the\n * document it would have been in, and a list that buries the `external-customer` ones among\n * thirty internal ones has hidden the only urgent line in it.\n */\n // webpieces-disable no-function-outside-class -- private static ordering of this class\n private static externalFirst(found: readonly ApiContractDefect[]): ApiContractDefect[] {\n return [...found].sort(\n (a: ApiContractDefect, b: ApiContractDefect) =>\n Number(b.isExternal()) - Number(a.isExternal()),\n );\n }\n}\n\n/**\n * Routes a defect to the rule that owns it.\n *\n * Every OpenAPI-level defect ALSO blocks MCP, so it is reported by whichever rule is running —\n * `api-rules-for-openapi` when that one is on, and `api-rules-for-mcp` alone when it is not. It is\n * never reported twice: a team running both would otherwise read every shared defect in two places\n * and have to work out that they are one.\n */\nclass DefectSink {\n constructor(\n private readonly openApi: DefectCollector | undefined,\n private readonly mcp: DefectCollector | undefined,\n ) {}\n\n /**\n * A defect that blocks the OpenAPI document, and therefore every tool on it too.\n *\n * The defect is BUILT from the rule that ends up reporting it, not handed in ready-made, because\n * a shared defect does not know in advance which rule will carry it: `api-rules-for-openapi` when\n * that one runs, and `api-rules-for-mcp` alone when it does not. A cure that named a fixed rule\n * would, on the mcp-only configuration, prescribe a `// webpieces-disable` line the collector\n * reading that site does not look for.\n */\n shared(build: (rule: string) => ApiContractDefect, site: Site): void {\n const target = this.openApi ?? this.mcp;\n if (target === undefined) return;\n target.add(build(target.rule()), site);\n }\n\n /** A defect that blocks ONE tool and nothing else. */\n mcpOnly(defect: ApiContractDefect, site: Site): void {\n this.mcp?.add(defect, site);\n }\n\n anyRuleRuns(): boolean {\n return this.openApi !== undefined || this.mcp !== undefined;\n }\n\n /** True when `api-rules-for-mcp` applies to this file — what the exclusion list is scoped to. */\n mcpRuns(): boolean {\n return this.mcp !== undefined;\n }\n}\n\n/**\n * Walks every project's `src`, extracts every `@ApiPath` contract with the generator's own\n * extractor, and judges the result against the two rules.\n *\n * ONE `ts.Program` over every contract file in the workspace, because a DTO a contract reaches\n * routinely lives in another project and the checker has to be able to follow the import — the same\n * reason the repo sweep in `@webpieces/api-doc-model`'s own spec builds one program rather than one\n * per file.\n */\nexport class ApiDocRulesScan {\n /**\n * Every `@InvalidEndpointForMcp` endpoint met on a file the MCP rule applies to.\n *\n * Collected even on a run with no findings at all, because restating them IS the feature: the\n * alternative considered in #1014 was a one-off warning when somebody adds one, and a warning\n * printed once at the moment of the decision is read by the one person who already knows.\n */\n private readonly exclusions: McpExclusion[] = [];\n\n constructor(\n private readonly workspaceRoot: string,\n private readonly projectInfos: Map<string, ProjectInfo>,\n /** OFF unless a caller read otherwise out of webpieces.config.json — see `defaultRules`. */\n private readonly openApiRule: ApiDocRule = ApiDocRule.off(OPENAPI_RULE),\n private readonly mcpRule: ApiDocRule = ApiDocRule.off(MCP_RULE),\n ) {}\n\n run(): ApiDocRulesFindings {\n if (!this.openApiRule.enabled && !this.mcpRule.enabled) return ApiDocRulesFindings.empty();\n const files = this.contractFiles();\n if (files.length === 0) return ApiDocRulesFindings.empty();\n\n const openApi = this.openApiRule.enabled ? new DefectCollector(OPENAPI_RULE) : undefined;\n const mcp = this.mcpRule.enabled ? new DefectCollector(MCP_RULE) : undefined;\n const program = ts.createProgram(\n files.map((file: ContractFile) => file.absPath),\n this.compilerOptions(),\n );\n const toolNames = new Map<string, string>();\n for (const file of files) {\n const sink = new DefectSink(\n file.openApi ? openApi : undefined,\n file.mcp ? mcp : undefined,\n );\n this.judgeFile(file, program, sink, toolNames);\n }\n return new ApiDocRulesFindings(\n openApi?.findings() ?? new ApiRuleFindings([], []),\n mcp?.findings() ?? new ApiRuleFindings([], []),\n this.exclusions,\n );\n }\n\n /** Every contract in one file, or the ONE refusal that stopped the file being read at all. */\n private judgeFile(\n file: ContractFile,\n program: ts.Program,\n sink: DefectSink,\n toolNames: Map<string, string>,\n ): void {\n if (!sink.anyRuleRuns()) return;\n const source = program.getSourceFile(file.absPath);\n if (source === undefined) return;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- an extraction refusal IS a finding; it is reported, not propagated\n try {\n for (const model of new ApiDocExtractor().extractAllFrom(program, source)) {\n this.judgeModel(model, source, sink, toolNames);\n }\n } catch (err: unknown) {\n //const error = toError(err);\n if (!(err instanceof ApiDocExtractionError)) throw err;\n this.reportExtractionFailure(err, file, source, sink);\n }\n }\n\n /**\n * An extraction that REFUSED. Reported under the shared list because it stops BOTH documents:\n * `@Endpoint` arguments that cannot be constant-folded, a bound on a non-numeric field, and the\n * `@ApiType(..., MCP)` ⇔ `@WpMcpTool` biconditional all fail here, before a model exists.\n */\n private reportExtractionFailure(\n error: ApiDocExtractionError,\n file: ContractFile,\n source: ts.SourceFile,\n sink: DefectSink,\n ): void {\n const site = Site.parse(error.location, file.absPath);\n sink.shared(\n (): ApiContractDefect => new ApiContractDefect(\n contractNamesIn(source)[0] ?? path.basename(file.absPath),\n '',\n `the contract cannot be read at all — ${error.message}`,\n site.relativeTo(this.workspaceRoot),\n error.cure,\n [],\n ),\n site,\n );\n }\n\n /** ONE contract: the shared OpenAPI checks, then the MCP-only ones. */\n private judgeModel(\n model: ApiDocModel,\n source: ts.SourceFile,\n sink: DefectSink,\n toolNames: Map<string, string>,\n ): void {\n const lines = contractLinesOf(source, model.contractName);\n this.judgeUnmapped(model, sink, source.fileName);\n this.judgeUnknownValues(model, sink, source.fileName);\n this.judgeRpcResponses(model, source, lines, sink);\n this.judgeTools(model, source, lines, sink, toolNames);\n }\n\n /** Everything `TypeResolver` could not represent — the generator's OWN verdict, re-worded. */\n private judgeUnmapped(model: ApiDocModel, sink: DefectSink, fallback: string): void {\n for (const unmapped of model.unmapped) {\n const site = Site.parse(unmapped.location, fallback);\n const verdict = classifyUnmapped(unmapped);\n sink.shared(\n (): ApiContractDefect => new ApiContractDefect(\n model.contractName,\n '',\n verdict.what,\n site.relativeTo(this.workspaceRoot),\n verdict.cure,\n model.apiTypes,\n ),\n site,\n );\n }\n }\n\n /** `Record<string, unknown>`, `unknown[]`, `x: unknown` — by VALUE TYPE, never by spelling. */\n private judgeUnknownValues(model: ApiDocModel, sink: DefectSink, fallback: string): void {\n for (const type of model.types.values()) {\n for (const field of type.fields) {\n if (!carriesUnknown(field.type)) continue;\n const site = Site.parse(field.location, fallback);\n sink.shared(\n (rule: string): ApiContractDefect =>\n new ApiContractDefect(\n model.contractName,\n '',\n `'${type.name}.${field.name}' publishes an 'unknown' value, so the ` +\n 'document states no shape for it at all',\n site.relativeTo(this.workspaceRoot),\n unknownValueCure(rule),\n model.apiTypes,\n ),\n site,\n );\n }\n }\n }\n\n /**\n * An RPC must NAME a response DTO, even an empty one.\n *\n * `Promise<void>` on a `cloudtasks` or `cron` endpoint is the CONTRACT — fire-and-forget, nothing\n * to shape — and is allowed. On an RPC it is a one-way door: a `void` response can never gain a\n * field without breaking every generated client, where `{}` grows additively forever. This is a\n * contract-EVOLUTION rule, which is why it is here and not in the MCP half: it is worth having on\n * an RPC that never becomes a tool.\n */\n private judgeRpcResponses(\n model: ApiDocModel,\n source: ts.SourceFile,\n lines: ContractLines,\n sink: DefectSink,\n ): void {\n for (const endpoint of model.endpoints) {\n if (endpoint.kind !== RPC_KIND) continue;\n if (endpoint.response !== undefined && !isVoidLike(endpoint.response)) continue;\n const site = new Site(source.fileName, lines.lineOf(endpoint.methodName));\n sink.shared(\n (): ApiContractDefect => new ApiContractDefect(\n model.contractName,\n endpoint.methodName,\n 'an RPC returns nothing a document can name (void, unknown, or no declared ' +\n 'return type)',\n site.relativeTo(this.workspaceRoot),\n VOID_RPC_CURE,\n model.apiTypes,\n ),\n site,\n );\n }\n }\n\n /** Everything `McpToolRegistry` refuses to boot on, plus whatever the renderer cannot render. */\n private judgeTools(\n model: ApiDocModel,\n source: ts.SourceFile,\n lines: ContractLines,\n sink: DefectSink,\n toolNames: Map<string, string>,\n ): void {\n const renderer = new McpSchemaRenderer(model);\n for (const endpoint of model.endpoints) {\n if (endpoint.invalidForMcp !== undefined) {\n if (!sink.mcpRuns()) continue;\n // @InvalidEndpointForMcp IS the answer to \"could this be a tool\". The decorator\n // carries the argument, so the rule asks nothing further and no webpieces-disable is\n // needed — a suppression would be a second, weaker spelling of the same declaration.\n this.exclusions.push(\n new McpExclusion(\n model.contractName,\n endpoint.methodName,\n endpoint.invalidForMcp,\n new Site(\n source.fileName,\n lines.lineOf(endpoint.methodName),\n ).relativeTo(this.workspaceRoot),\n ),\n );\n continue;\n }\n if (endpoint.mcpTool === undefined) continue;\n const site = new Site(source.fileName, lines.lineOf(endpoint.methodName));\n for (const failure of toolFailures(endpoint, renderer, toolNames, model)) {\n sink.mcpOnly(\n new ApiContractDefect(\n model.contractName,\n endpoint.methodName,\n failure.what,\n site.relativeTo(this.workspaceRoot),\n failure.cure,\n model.apiTypes,\n ),\n site,\n );\n }\n }\n for (const methodName of lines.toolsWithoutEndpoint) {\n const site = new Site(source.fileName, lines.lineOf(methodName));\n sink.mcpOnly(\n new ApiContractDefect(\n model.contractName,\n methodName,\n 'carries @WpMcpTool but is not an @Endpoint, so it is not routed at all',\n site.relativeTo(this.workspaceRoot),\n \"Add @Endpoint(POST, '/path', READ, RPC) to it, or drop the @WpMcpTool.\",\n model.apiTypes,\n ),\n site,\n );\n }\n }\n\n /** Every non-test `.ts` under a project's `src` whose text DECLARES an `@ApiPath` contract. */\n private contractFiles(): ContractFile[] {\n const found: ContractFile[] = [];\n for (const info of this.projectInfos.values()) {\n if (info.root === '' || info.root === '.') continue;\n const openApi =\n this.openApiRule.enabled &&\n !matchesAnyGlob(info.root, this.openApiRule.allowedPaths);\n const mcp =\n this.mcpRule.enabled && !matchesAnyGlob(info.root, this.mcpRule.allowedPaths);\n if (!openApi && !mcp) continue;\n const srcDir = path.join(path.resolve(this.workspaceRoot, info.root), 'src');\n if (!fs.existsSync(srcDir)) continue;\n for (const file of collectTsFiles(srcDir)) {\n if (isTestFile(file)) continue; // a fixture is not a published contract\n if (!DECLARES_CONTRACT.test(fs.readFileSync(file, 'utf8'))) continue;\n found.push(new ContractFile(file, openApi, mcp));\n }\n }\n return found.sort((a: ContractFile, b: ContractFile) => a.absPath.localeCompare(b.absPath));\n }\n\n /**\n * `tsconfig.base.json`'s options when the workspace has one, so an `@webpieces/*` import in a\n * contract RESOLVES and the checker can follow a DTO into another project. Without that the\n * resolver reports every cross-project type as unmapped, which would be a rule failing on its\n * own inability to read rather than on anything the author wrote.\n */\n private compilerOptions(): ts.CompilerOptions {\n const base = path.join(this.workspaceRoot, 'tsconfig.base.json');\n const declared = fs.existsSync(base)\n ? ts.parseJsonConfigFileContent(\n ts.readConfigFile(base, ts.sys.readFile).config,\n ts.sys,\n this.workspaceRoot,\n ).options\n : {};\n return {\n ...declared,\n noEmit: true,\n skipLibCheck: true,\n types: [],\n experimentalDecorators: true,\n };\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"api-doc-rules-scan.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-doc-rules-scan.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `api-rules-for-openapi` and `api-rules-for-mcp` (#1011) — the CI half of \"is this contract\n * publishable\", on every `@ApiPath` class IN SCOPE, `@ApiType` or not.\n *\n * \"In scope\" is the rule's `mode` (#1017): `AFFECTED_PROJECT` scans the contracts of the projects the\n * diff touched — the granularity nx already builds at, and the mode a consumer normally picks —\n * while `RUN_EVERY_TIME` scans the whole workspace for a migration sweep. `ApiDocRule.coversProject`\n * is the one place that answers it, for both rules.\n *\n * ## The acceptance contract, and why this file drives the generator instead of copying it\n *\n * The rules exist so that ADDING `@ApiType(...)` (and `@WpMcpTool`) to a contract that passes them\n * always works. That is only true if \"expressible\" has exactly ONE definition, so this scan runs the\n * generator's own code — `ApiDocExtractor` / `TypeResolver` from `@webpieces/api-doc-model` for the\n * OpenAPI half, and `McpSchemaRenderer` tool-by-tool for the MCP half. A second implementation of\n * \"what can be published\" would drift from the first on the release that improved either one, and\n * the drift would be silent: the rules would stay green while generation started failing. The two\n * packages ship on the same release train, so the dependency is in lockstep by construction.\n *\n * Two things here are STRICTER than the generator, deliberately, and both are publishing rules\n * rather than expressibility ones (being stricter cannot break the acceptance contract — it can only\n * refuse something that would have generated):\n *\n * - an `unknown` VALUE TYPE anywhere (`Record<string, unknown>`, `unknown[]`, a bare `unknown`\n * field). The extractor maps it to a primitive and the generator publishes `{}`, which in JSON\n * Schema means \"anything\" — a partner-facing field with no shape, which is the defect the\n * unmapped guard exists for, arriving through a door the guard does not watch.\n * - an `rpc` or `external` endpoint whose response is `void`. Fire-and-forget is the CONTRACT of a\n * `cloudtasks` or `cron` endpoint and is allowed there; an endpoint somebody WAITS on that answers\n * nothing can never gain a field without a breaking change, where a named empty response object\n * grows additively forever (#1017 — #1016 read this narrowly as rpc-only because `external` was\n * unstated).\n *\n * ## Why it lives in the rules engine and not in the doc parser\n *\n * `@webpieces/api-doc-model` is only ever pointed at contracts somebody chose to publish. `@ApiType`\n * is a PUBLISHING decision added later, on purpose — so a shape rule that only ran on contracts which\n * had already opted in would let a team discover, six months afterwards, that the type was never\n * expressible, by which time it is in partners' generated clients. So every check below runs on every\n * contract in scope, opted in or not — `@ApiType` narrows nothing here.\n *\n * ## Root-level unions are NOT re-checked here\n *\n * `no-root-union-api-type` (#1009) already refuses them, workspace-wide, with its own config key and\n * its own per-site hatch. One implementation. The MCP half still reports one when it meets it,\n * because `McpSchemaRenderer` refuses it as its own backstop and this scan reports whatever the\n * renderer says — which is the correct division: the OpenAPI document publishes a root union\n * perfectly well, and only a tool schema cannot carry one.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport * as ts from 'typescript';\nimport {\n ApiDocExtractionError,\n ApiDocExtractor,\n ApiDocModel,\n DocumentedEndpoint,\n McpRenderError,\n McpSchemaRenderer,\n TypeRef,\n UnmappedType,\n} from '@webpieces/api-doc-model';\nimport { ProjectInfo } from '../project-info';\nimport { collectTsFiles, isTestFile } from './api-ast';\nimport {\n ApiContractDefect,\n ApiDocRule,\n ApiDocRulesFindings,\n ApiRuleFindings,\n DisableComment,\n McpExclusion,\n MCP_RULE,\n OPENAPI_RULE,\n} from './api-doc-rules';\nimport {\n ANSWERING_KINDS,\n ContractLines,\n unknownValueCure,\n VOID_RPC_CURE,\n carriesUnknown,\n classifyUnmapped,\n contractLinesOf,\n contractNamesIn,\n isVoidLike,\n toolFailures,\n} from './api-doc-rules-verdicts';\n\n/** `@ApiPath(` at COLUMN ZERO — a docstring that TALKS about a contract declares none. */\nconst DECLARES_CONTRACT = /^@ApiPath\\(/m;\n\n/** ONE contract file, and which of the two rules apply to the project that owns it. */\nclass ContractFile {\n constructor(\n public readonly absPath: string,\n public readonly openApi: boolean,\n public readonly mcp: boolean,\n ) {}\n}\n\n/** Where one declaration sits, already split out of the extractor's `File.ts:LINE:COL` spelling. */\nclass Site {\n constructor(\n public readonly absPath: string,\n public readonly line: number,\n ) {}\n\n /** `path/to/File.ts:LINE`, workspace-relative — what a refusal prints. */\n relativeTo(workspaceRoot: string): string {\n return `${path.relative(workspaceRoot, this.absPath)}:${this.line}`;\n }\n\n /** `abs/File.ts:12:5` -> a Site. An unparseable one falls back to line 1 of `fallback`. */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static parse(location: string, fallback: string): Site {\n const match = location.match(/^(.*):(\\d+):\\d+$/);\n if (match === null) return new Site(fallback, 1);\n return new Site(match[1], Number(match[2]));\n }\n}\n\n/**\n * ONE rule's accumulator. It is what applies the per-site disable, so the disable semantics live in\n * exactly one place and cannot differ between the two rules.\n */\nclass DefectCollector {\n private readonly violations: ApiContractDefect[] = [];\n private readonly reasonless: ApiContractDefect[] = [];\n\n constructor(private readonly ruleName: string) {}\n\n /** The rule this collector reports under — what a shared defect's cure has to name. */\n rule(): string {\n return this.ruleName;\n }\n\n add(defect: ApiContractDefect, site: Site): void {\n const disable = DisableComment.readAt(site.absPath, site.line, this.ruleName);\n if (disable === undefined) {\n this.violations.push(defect);\n return;\n }\n if (!disable.hasReason) this.reasonless.push(defect);\n }\n\n findings(): ApiRuleFindings {\n return new ApiRuleFindings(\n DefectCollector.externalFirst(this.violations),\n DefectCollector.externalFirst(this.reasonless),\n );\n }\n\n /**\n * Partner-facing contracts first. The same defect is a different size depending on who reads the\n * document it would have been in, and a list that buries the `external-customer` ones among\n * thirty internal ones has hidden the only urgent line in it.\n */\n // webpieces-disable no-function-outside-class -- private static ordering of this class\n private static externalFirst(found: readonly ApiContractDefect[]): ApiContractDefect[] {\n return [...found].sort(\n (a: ApiContractDefect, b: ApiContractDefect) =>\n Number(b.isExternal()) - Number(a.isExternal()),\n );\n }\n}\n\n/**\n * Routes a defect to the rule that owns it.\n *\n * Every OpenAPI-level defect ALSO blocks MCP, so it is reported by whichever rule is running —\n * `api-rules-for-openapi` when that one is on, and `api-rules-for-mcp` alone when it is not. It is\n * never reported twice: a team running both would otherwise read every shared defect in two places\n * and have to work out that they are one.\n */\nclass DefectSink {\n constructor(\n private readonly openApi: DefectCollector | undefined,\n private readonly mcp: DefectCollector | undefined,\n ) {}\n\n /**\n * A defect that blocks the OpenAPI document, and therefore every tool on it too.\n *\n * The defect is BUILT from the rule that ends up reporting it, not handed in ready-made, because\n * a shared defect does not know in advance which rule will carry it: `api-rules-for-openapi` when\n * that one runs, and `api-rules-for-mcp` alone when it does not. A cure that named a fixed rule\n * would, on the mcp-only configuration, prescribe a `// webpieces-disable` line the collector\n * reading that site does not look for.\n */\n shared(build: (rule: string) => ApiContractDefect, site: Site): void {\n const target = this.openApi ?? this.mcp;\n if (target === undefined) return;\n target.add(build(target.rule()), site);\n }\n\n /** A defect that blocks ONE tool and nothing else. */\n mcpOnly(defect: ApiContractDefect, site: Site): void {\n this.mcp?.add(defect, site);\n }\n\n anyRuleRuns(): boolean {\n return this.openApi !== undefined || this.mcp !== undefined;\n }\n\n /** True when `api-rules-for-mcp` applies to this file — what the exclusion list is scoped to. */\n mcpRuns(): boolean {\n return this.mcp !== undefined;\n }\n}\n\n/**\n * Walks every project's `src`, extracts every `@ApiPath` contract with the generator's own\n * extractor, and judges the result against the two rules.\n *\n * ONE `ts.Program` over every contract file in the workspace, because a DTO a contract reaches\n * routinely lives in another project and the checker has to be able to follow the import — the same\n * reason the repo sweep in `@webpieces/api-doc-model`'s own spec builds one program rather than one\n * per file.\n */\nexport class ApiDocRulesScan {\n /**\n * Every `@InvalidEndpointForMcp` endpoint met on a file the MCP rule applies to.\n *\n * Collected even on a run with no findings at all, because restating them IS the feature: the\n * alternative considered in #1014 was a one-off warning when somebody adds one, and a warning\n * printed once at the moment of the decision is read by the one person who already knows.\n */\n private readonly exclusions: McpExclusion[] = [];\n\n constructor(\n private readonly workspaceRoot: string,\n private readonly projectInfos: Map<string, ProjectInfo>,\n /** OFF unless a caller read otherwise out of webpieces.config.json, which MUST state it. */\n private readonly openApiRule: ApiDocRule = ApiDocRule.off(OPENAPI_RULE),\n private readonly mcpRule: ApiDocRule = ApiDocRule.off(MCP_RULE),\n ) {}\n\n run(): ApiDocRulesFindings {\n if (!this.openApiRule.enabled && !this.mcpRule.enabled) return ApiDocRulesFindings.empty();\n const files = this.contractFiles();\n if (files.length === 0) return ApiDocRulesFindings.empty();\n\n const openApi = this.openApiRule.enabled ? new DefectCollector(OPENAPI_RULE) : undefined;\n const mcp = this.mcpRule.enabled ? new DefectCollector(MCP_RULE) : undefined;\n const program = ts.createProgram(\n files.map((file: ContractFile) => file.absPath),\n this.compilerOptions(),\n );\n const toolNames = new Map<string, string>();\n for (const file of files) {\n const sink = new DefectSink(\n file.openApi ? openApi : undefined,\n file.mcp ? mcp : undefined,\n );\n this.judgeFile(file, program, sink, toolNames);\n }\n return new ApiDocRulesFindings(\n openApi?.findings() ?? new ApiRuleFindings([], []),\n mcp?.findings() ?? new ApiRuleFindings([], []),\n this.exclusions,\n );\n }\n\n /** Every contract in one file, or the ONE refusal that stopped the file being read at all. */\n private judgeFile(\n file: ContractFile,\n program: ts.Program,\n sink: DefectSink,\n toolNames: Map<string, string>,\n ): void {\n if (!sink.anyRuleRuns()) return;\n const source = program.getSourceFile(file.absPath);\n if (source === undefined) return;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- an extraction refusal IS a finding; it is reported, not propagated\n try {\n for (const model of new ApiDocExtractor().extractAllFrom(program, source)) {\n this.judgeModel(model, source, sink, toolNames);\n }\n } catch (err: unknown) {\n //const error = toError(err);\n if (!(err instanceof ApiDocExtractionError)) throw err;\n this.reportExtractionFailure(err, file, source, sink);\n }\n }\n\n /**\n * An extraction that REFUSED. Reported under the shared list because it stops BOTH documents:\n * `@Endpoint` arguments that cannot be constant-folded, a bound on a non-numeric field, and the\n * `@ApiType(..., MCP)` ⇔ `@WpMcpTool` biconditional all fail here, before a model exists.\n */\n private reportExtractionFailure(\n error: ApiDocExtractionError,\n file: ContractFile,\n source: ts.SourceFile,\n sink: DefectSink,\n ): void {\n const site = Site.parse(error.location, file.absPath);\n sink.shared(\n (): ApiContractDefect => new ApiContractDefect(\n contractNamesIn(source)[0] ?? path.basename(file.absPath),\n '',\n `the contract cannot be read at all — ${error.message}`,\n site.relativeTo(this.workspaceRoot),\n error.cure,\n [],\n ),\n site,\n );\n }\n\n /** ONE contract: the shared OpenAPI checks, then the MCP-only ones. */\n private judgeModel(\n model: ApiDocModel,\n source: ts.SourceFile,\n sink: DefectSink,\n toolNames: Map<string, string>,\n ): void {\n const lines = contractLinesOf(source, model.contractName);\n this.judgeUnmapped(model, sink, source.fileName);\n this.judgeUnknownValues(model, sink, source.fileName);\n this.judgeAnsweringResponses(model, source, lines, sink);\n this.judgeTools(model, source, lines, sink, toolNames);\n }\n\n /** Everything `TypeResolver` could not represent — the generator's OWN verdict, re-worded. */\n private judgeUnmapped(model: ApiDocModel, sink: DefectSink, fallback: string): void {\n for (const unmapped of model.unmapped) {\n const site = Site.parse(unmapped.location, fallback);\n const verdict = classifyUnmapped(unmapped);\n sink.shared(\n (): ApiContractDefect => new ApiContractDefect(\n model.contractName,\n '',\n verdict.what,\n site.relativeTo(this.workspaceRoot),\n verdict.cure,\n model.apiTypes,\n ),\n site,\n );\n }\n }\n\n /** `Record<string, unknown>`, `unknown[]`, `x: unknown` — by VALUE TYPE, never by spelling. */\n private judgeUnknownValues(model: ApiDocModel, sink: DefectSink, fallback: string): void {\n for (const type of model.types.values()) {\n for (const field of type.fields) {\n if (!carriesUnknown(field.type)) continue;\n const site = Site.parse(field.location, fallback);\n sink.shared(\n (rule: string): ApiContractDefect =>\n new ApiContractDefect(\n model.contractName,\n '',\n `'${type.name}.${field.name}' publishes an 'unknown' value, so the ` +\n 'document states no shape for it at all',\n site.relativeTo(this.workspaceRoot),\n unknownValueCure(rule),\n model.apiTypes,\n ),\n site,\n );\n }\n }\n }\n\n /**\n * An endpoint somebody WAITS ON must NAME a response DTO, even an empty one.\n *\n * `Promise<void>` on a `cloudtasks` or `cron` endpoint is the CONTRACT — fire-and-forget, nothing\n * to shape — and is allowed. On an `rpc` OR an `external` it is a one-way door: both are\n * synchronous request/response, an outside caller reads what comes back, and a `void` response\n * can never gain a field without breaking every generated client where `{}` grows additively\n * forever. This is a contract-EVOLUTION rule, which is why it is here and not in the MCP half: it\n * is worth having on an endpoint that never becomes a tool. See ANSWERING_KINDS (#1017).\n */\n private judgeAnsweringResponses(\n model: ApiDocModel,\n source: ts.SourceFile,\n lines: ContractLines,\n sink: DefectSink,\n ): void {\n for (const endpoint of model.endpoints) {\n // `.some` and not `.includes`: the model's `kind` is a widened string, and the list is\n // typed EndpointKind so a kind that stops existing is a compile error here.\n if (!ANSWERING_KINDS.some((kind: string): boolean => kind === endpoint.kind)) continue;\n if (endpoint.response !== undefined && !isVoidLike(endpoint.response)) continue;\n const site = new Site(source.fileName, lines.lineOf(endpoint.methodName));\n sink.shared(\n (): ApiContractDefect => new ApiContractDefect(\n model.contractName,\n endpoint.methodName,\n `an ${endpoint.kind} endpoint returns nothing a document can name (void, ` +\n 'unknown, or no declared return type)',\n site.relativeTo(this.workspaceRoot),\n VOID_RPC_CURE,\n model.apiTypes,\n ),\n site,\n );\n }\n }\n\n /** Everything `McpToolRegistry` refuses to boot on, plus whatever the renderer cannot render. */\n private judgeTools(\n model: ApiDocModel,\n source: ts.SourceFile,\n lines: ContractLines,\n sink: DefectSink,\n toolNames: Map<string, string>,\n ): void {\n const renderer = new McpSchemaRenderer(model);\n for (const endpoint of model.endpoints) {\n if (endpoint.invalidForMcp !== undefined) {\n if (!sink.mcpRuns()) continue;\n // @InvalidEndpointForMcp IS the answer to \"could this be a tool\". The decorator\n // carries the argument, so the rule asks nothing further and no webpieces-disable is\n // needed — a suppression would be a second, weaker spelling of the same declaration.\n this.exclusions.push(\n new McpExclusion(\n model.contractName,\n endpoint.methodName,\n endpoint.invalidForMcp,\n new Site(\n source.fileName,\n lines.lineOf(endpoint.methodName),\n ).relativeTo(this.workspaceRoot),\n ),\n );\n continue;\n }\n if (endpoint.mcpTool === undefined) continue;\n const site = new Site(source.fileName, lines.lineOf(endpoint.methodName));\n for (const failure of toolFailures(endpoint, renderer, toolNames, model)) {\n sink.mcpOnly(\n new ApiContractDefect(\n model.contractName,\n endpoint.methodName,\n failure.what,\n site.relativeTo(this.workspaceRoot),\n failure.cure,\n model.apiTypes,\n ),\n site,\n );\n }\n }\n for (const methodName of lines.toolsWithoutEndpoint) {\n const site = new Site(source.fileName, lines.lineOf(methodName));\n sink.mcpOnly(\n new ApiContractDefect(\n model.contractName,\n methodName,\n 'carries @WpMcpTool but is not an @Endpoint, so it is not routed at all',\n site.relativeTo(this.workspaceRoot),\n \"Add @Endpoint(POST, '/path', READ, RPC) to it, or drop the @WpMcpTool.\",\n model.apiTypes,\n ),\n site,\n );\n }\n }\n\n /** Every non-test `.ts` under a project's `src` whose text DECLARES an `@ApiPath` contract. */\n private contractFiles(): ContractFile[] {\n const found: ContractFile[] = [];\n for (const info of this.projectInfos.values()) {\n if (info.root === '' || info.root === '.') continue;\n // ONE question, asked of the rule itself: `mode` (OFF / AFFECTED_PROJECT /\n // RUN_EVERY_TIME) and `allowedPaths` are both folded into coversProject, so the\n // affected-project narrowing cannot be honoured in one branch and skipped in the other.\n const openApi = this.openApiRule.coversProject(info.root);\n const mcp = this.mcpRule.coversProject(info.root);\n if (!openApi && !mcp) continue;\n const srcDir = path.join(path.resolve(this.workspaceRoot, info.root), 'src');\n if (!fs.existsSync(srcDir)) continue;\n for (const file of collectTsFiles(srcDir)) {\n if (isTestFile(file)) continue; // a fixture is not a published contract\n if (!DECLARES_CONTRACT.test(fs.readFileSync(file, 'utf8'))) continue;\n found.push(new ContractFile(file, openApi, mcp));\n }\n }\n return found.sort((a: ContractFile, b: ContractFile) => a.absPath.localeCompare(b.absPath));\n }\n\n /**\n * `tsconfig.base.json`'s options when the workspace has one, so an `@webpieces/*` import in a\n * contract RESOLVES and the checker can follow a DTO into another project. Without that the\n * resolver reports every cross-project type as unmapped, which would be a rule failing on its\n * own inability to read rather than on anything the author wrote.\n */\n private compilerOptions(): ts.CompilerOptions {\n const base = path.join(this.workspaceRoot, 'tsconfig.base.json');\n const declared = fs.existsSync(base)\n ? ts.parseJsonConfigFileContent(\n ts.readConfigFile(base, ts.sys.readFile).config,\n ts.sys,\n this.workspaceRoot,\n ).options\n : {};\n return {\n ...declared,\n noEmit: true,\n skipLibCheck: true,\n types: [],\n experimentalDecorators: true,\n };\n }\n}\n"]}
|
|
@@ -18,6 +18,19 @@ import { EndpointKind } from './api-relations';
|
|
|
18
18
|
* set the literal belongs to is pinned by that type.
|
|
19
19
|
*/
|
|
20
20
|
export declare const RPC_KIND: EndpointKind;
|
|
21
|
+
/**
|
|
22
|
+
* The endpoint kinds whose response a caller WAITS FOR, and which therefore must name a DTO (#1017).
|
|
23
|
+
*
|
|
24
|
+
* `rpc` and `external` are both synchronous request/response: somebody posts and reads what comes
|
|
25
|
+
* back, so there IS a body and it must be a named type that can gain a field later. `void` on either
|
|
26
|
+
* is a one-way door — a `void` response can never grow without breaking every generated client, where
|
|
27
|
+
* an empty DTO grows additively forever. #1016 refused it on `rpc` only, because `external` was
|
|
28
|
+
* unstated at the time; it is stated now.
|
|
29
|
+
*
|
|
30
|
+
* `cloudtasks` and `cron` are deliberately NOT here: delivery is fire-and-forget, nobody waits for a
|
|
31
|
+
* body, and `Promise<void>` is the CONTRACT rather than an omission.
|
|
32
|
+
*/
|
|
33
|
+
export declare const ANSWERING_KINDS: readonly EndpointKind[];
|
|
21
34
|
/** ONE reason a declaration is refused, in the two halves every refusal prints. */
|
|
22
35
|
export declare class Verdict {
|
|
23
36
|
readonly what: string;
|
|
@@ -46,7 +59,7 @@ export declare class ContractLines {
|
|
|
46
59
|
* failure mode a printed cure must not have.
|
|
47
60
|
*/
|
|
48
61
|
export declare function unknownValueCure(rule: string): string;
|
|
49
|
-
/** The one cure for a `void`
|
|
62
|
+
/** The one cure for a `void` endpoint that somebody is WAITING on (rpc or external). */
|
|
50
63
|
export declare const VOID_RPC_CURE: string;
|
|
51
64
|
/**
|
|
52
65
|
* Why the resolver could not map this type, said in the author's vocabulary.
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* form" cannot give an author.
|
|
12
12
|
*/
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
-
exports.VOID_RPC_CURE = exports.ContractLines = exports.Verdict = exports.RPC_KIND = void 0;
|
|
14
|
+
exports.VOID_RPC_CURE = exports.ContractLines = exports.Verdict = exports.ANSWERING_KINDS = exports.RPC_KIND = void 0;
|
|
15
15
|
exports.unknownValueCure = unknownValueCure;
|
|
16
16
|
exports.classifyUnmapped = classifyUnmapped;
|
|
17
17
|
exports.carriesUnknown = carriesUnknown;
|
|
@@ -28,6 +28,19 @@ const api_doc_model_1 = require("@webpieces/api-doc-model");
|
|
|
28
28
|
* set the literal belongs to is pinned by that type.
|
|
29
29
|
*/
|
|
30
30
|
exports.RPC_KIND = 'rpc';
|
|
31
|
+
/**
|
|
32
|
+
* The endpoint kinds whose response a caller WAITS FOR, and which therefore must name a DTO (#1017).
|
|
33
|
+
*
|
|
34
|
+
* `rpc` and `external` are both synchronous request/response: somebody posts and reads what comes
|
|
35
|
+
* back, so there IS a body and it must be a named type that can gain a field later. `void` on either
|
|
36
|
+
* is a one-way door — a `void` response can never grow without breaking every generated client, where
|
|
37
|
+
* an empty DTO grows additively forever. #1016 refused it on `rpc` only, because `external` was
|
|
38
|
+
* unstated at the time; it is stated now.
|
|
39
|
+
*
|
|
40
|
+
* `cloudtasks` and `cron` are deliberately NOT here: delivery is fire-and-forget, nobody waits for a
|
|
41
|
+
* body, and `Promise<void>` is the CONTRACT rather than an omission.
|
|
42
|
+
*/
|
|
43
|
+
exports.ANSWERING_KINDS = ['rpc', 'external'];
|
|
31
44
|
/** The scalar keywords a MIXED SCALAR union is made of. */
|
|
32
45
|
const SCALAR_KEYWORDS = new Set([
|
|
33
46
|
ts.SyntaxKind.StringKeyword,
|
|
@@ -80,11 +93,13 @@ function unknownValueCure(rule) {
|
|
|
80
93
|
'An existing no-any-unknown disable does NOT count — that rule asked whether the code is ' +
|
|
81
94
|
'type-safe, which is a different question from whether a partner is handed a shapeless field.');
|
|
82
95
|
}
|
|
83
|
-
/** The one cure for a `void`
|
|
96
|
+
/** The one cure for a `void` endpoint that somebody is WAITING on (rpc or external). */
|
|
84
97
|
exports.VOID_RPC_CURE = 'Declare a named response DTO and return Promise<That>, even when it has no fields today: an ' +
|
|
85
98
|
'empty object grows additively forever, and a void response cannot gain a field without ' +
|
|
86
|
-
'breaking every generated client.
|
|
87
|
-
'
|
|
99
|
+
'breaking every generated client. An external endpoint is a synchronous call an outside caller ' +
|
|
100
|
+
'waits on, so it has a body too — an empty one serializes as {} and the failure detail rides on ' +
|
|
101
|
+
'the thrown error. Promise<void> stays legal on a cloudtasks or cron endpoint, where ' +
|
|
102
|
+
'fire-and-forget is the contract.';
|
|
88
103
|
/**
|
|
89
104
|
* Why the resolver could not map this type, said in the author's vocabulary.
|
|
90
105
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-doc-rules-verdicts.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-doc-rules-verdicts.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The pure VERDICTS the api-contract rules print, split out of `api-doc-rules-scan.ts`, which owns\n * the walk and is at its file-size limit.\n *\n * Nothing here touches the filesystem or the project graph. Each function answers one question about\n * ONE declaration the extractor or the renderer has already ruled on, and turns it into the two\n * halves every webpieces refusal carries — what is wrong, and what to do instead. The DECISION is\n * never made here: `model.unmapped` is the extractor's and a render failure is the renderer's. What\n * this file adds is the WORDING, which is the part a generic \"no model representation for this type\n * form\" cannot give an author.\n */\n\nimport * as ts from 'typescript';\nimport {\n ApiDocModel,\n DocumentedEndpoint,\n McpRenderError,\n McpSchemaRenderer,\n TypeRef,\n UnmappedType,\n} from '@webpieces/api-doc-model';\nimport { EndpointKind } from './api-relations';\n\n/**\n * The one trigger kind an MCP tool may have. A LITERAL, for the same reason `EndpointKind` beside it\n * is one: this package deliberately does not depend on `@webpieces/core-util` at runtime, and the\n * set the literal belongs to is pinned by that type.\n */\nexport const RPC_KIND: EndpointKind = 'rpc';\n\n/** The scalar keywords a MIXED SCALAR union is made of. */\nconst SCALAR_KEYWORDS = new Set<ts.SyntaxKind>([\n ts.SyntaxKind.StringKeyword,\n ts.SyntaxKind.NumberKeyword,\n ts.SyntaxKind.BooleanKeyword,\n]);\n\n/** ONE reason a declaration is refused, in the two halves every refusal prints. */\nexport class Verdict {\n constructor(\n public readonly what: string,\n public readonly cure: string,\n ) {}\n}\n\n\n/** Method name -> the line its declaration starts on, for one contract class. */\nexport class ContractLines {\n constructor(\n public readonly classLine: number,\n public readonly byMethod: ReadonlyMap<string, number>,\n /** Methods carrying `@WpMcpTool` that do NOT also carry `@Endpoint`. */\n public readonly toolsWithoutEndpoint: readonly string[],\n ) {}\n\n lineOf(methodName: string): number {\n return this.byMethod.get(methodName) ?? this.classLine;\n }\n}\n\n\n/**\n * The cure for an `unknown` value, for whichever rule is REPORTING it.\n *\n * It takes the rule name rather than naming one, because this defect is shared: it blocks the\n * OpenAPI document, so `DefectSink` routes it to `api-rules-for-openapi` when that rule is running\n * and to `api-rules-for-mcp` when it is the only one. A hard-coded rule name would then hand an\n * author, on the mcp-only configuration, a `// webpieces-disable api-rules-for-openapi` line that\n * the collector reading the site does not look for — a cure that does not cure, which is the one\n * failure mode a printed cure must not have.\n */\n// webpieces-disable no-function-outside-class -- pure message builder, beside the verdicts that print it\nexport function unknownValueCure(rule: string): string {\n return (\n 'Name the value type — a DTO, an array of one, a string-literal union, or ' +\n 'Record<string, ThatDto>. If the shape really is unknowable (a transport envelope, ' +\n 'arbitrary SQL rows), write the per-site disable and say so: ' +\n `// webpieces-disable ${rule} -- <why this field is published with no shape>. ` +\n 'An existing no-any-unknown disable does NOT count — that rule asked whether the code is ' +\n 'type-safe, which is a different question from whether a partner is handed a shapeless field.'\n );\n}\n\n/** The one cure for a `void` RPC. */\nexport const VOID_RPC_CURE =\n 'Declare a named response DTO and return Promise<That>, even when it has no fields today: an ' +\n 'empty object grows additively forever, and a void response cannot gain a field without ' +\n 'breaking every generated client. Promise<void> stays legal on a cloudtasks or cron endpoint, ' +\n 'where fire-and-forget is the contract.';\n\n/**\n * Why the resolver could not map this type, said in the author's vocabulary.\n *\n * The DECISION to refuse is the extractor's and is not second-guessed here — every entry in\n * `model.unmapped` becomes a defect. What this adds is the MESSAGE: an open enum and a mixed scalar\n * union are the two shapes a real repo actually hits (measured: 6 of 98 endpoints, from exactly\n * these two), and \"no model representation for this type form\" tells their author nothing.\n */\n// webpieces-disable no-function-outside-class -- pure classifier over the extractor's own verdict\nexport function classifyUnmapped(unmapped: UnmappedType): Verdict {\n const branches = unionBranchesOf(unmapped.typeText);\n if (isOpenEnum(branches)) {\n return new Verdict(\n `'${unmapped.typeText}' is an OPEN ENUM — literals unioned with the wide type`,\n 'Use an enum OR a string, not both. TypeScript COLLAPSES this union to the wide type, ' +\n 'so the literals buy no compile-time safety and a document cannot state them as a ' +\n \"closed set. Either drop the `| string` (`'a' | 'b'` publishes as a real enum), or \" +\n 'drop the literals and list the known values in the JSDoc.',\n );\n }\n if (isMixedScalarUnion(branches)) {\n return new Verdict(\n `'${unmapped.typeText}' is a MIXED SCALAR union`,\n 'Give the field ONE type. JSON Schema can spell {\"type\": [\"string\",\"number\"]}, but ' +\n 'this framework\\'s ApiJsonSchema helpers read a type array as \"one real type plus ' +\n 'maybe null\" (baseTypeOf returns the first non-null member), so publishing it ' +\n 'would VALIDATE WRONGLY — which is worse than refusing it. Pick the type, or wrap ' +\n 'the alternatives in a discriminated union of named objects.',\n );\n }\n return new Verdict(\n `'${unmapped.typeText}' has no shape a document can state — ${unmapped.reason}`,\n 'Give it a type a document can carry: a named DTO, an array of one, a string-literal ' +\n 'union, a Record<string, ThatDto>, or a primitive. A field with no shape publishes as ' +\n '\"anything\".',\n );\n}\n\n/** The branches of `typeText` when it parses as a union, else an empty list. */\n// webpieces-disable no-function-outside-class -- pure parser used by classifyUnmapped alone\nfunction unionBranchesOf(typeText: string): readonly ts.TypeNode[] {\n const parsed = ts.createSourceFile(\n '__unmapped.ts',\n `type __X = ${typeText};`,\n ts.ScriptTarget.Latest,\n true,\n );\n const alias = parsed.statements[0];\n if (alias === undefined || !ts.isTypeAliasDeclaration(alias)) return [];\n if (!ts.isUnionTypeNode(alias.type)) return [];\n return alias.type.types.filter((branch: ts.TypeNode) => !isNullishBranch(branch));\n}\n\n/** `'a' | 'b' | string`, and the numeric equivalent. See #1010 and the decision on #1011. */\n// webpieces-disable no-function-outside-class -- pure predicate beside classifyUnmapped\nfunction isOpenEnum(branches: readonly ts.TypeNode[]): boolean {\n const wide = branches.some(\n (branch: ts.TypeNode) =>\n branch.kind === ts.SyntaxKind.StringKeyword ||\n branch.kind === ts.SyntaxKind.NumberKeyword,\n );\n const literals = branches.some((branch: ts.TypeNode) => ts.isLiteralTypeNode(branch));\n return wide && literals;\n}\n\n/** `string | number`, `string | boolean | null` — two or more DIFFERENT scalar keywords. */\n// webpieces-disable no-function-outside-class -- pure predicate beside classifyUnmapped\nfunction isMixedScalarUnion(branches: readonly ts.TypeNode[]): boolean {\n if (branches.length < 2) return false;\n const kinds = new Set<ts.SyntaxKind>();\n for (const branch of branches) {\n if (!SCALAR_KEYWORDS.has(branch.kind)) return false;\n kinds.add(branch.kind);\n }\n return kinds.size >= 2;\n}\n\n/** `null` / `undefined` branches — nullability, not composition, exactly as the resolver reads it. */\n// webpieces-disable no-function-outside-class -- pure predicate beside classifyUnmapped\nfunction isNullishBranch(node: ts.TypeNode): boolean {\n if (node.kind === ts.SyntaxKind.UndefinedKeyword || node.kind === ts.SyntaxKind.NullKeyword) {\n return true;\n }\n return ts.isLiteralTypeNode(node) && node.literal.kind === ts.SyntaxKind.NullKeyword;\n}\n\n/**\n * True when this type PUBLISHES an `unknown` value.\n *\n * It looks at the VALUE TYPE wherever it sits — the item of an array, the value of an open map, the\n * field itself — and never at the `Record<>` spelling, because the real cases in the wild are not\n * all Records: `params?: unknown[]` is the same defect written differently.\n *\n * It does NOT descend through a `ref`: a named DTO is its own entry in the model and its own fields\n * are judged there, so descending would report the same field once per type that points at it.\n */\n// webpieces-disable no-function-outside-class -- pure predicate over one TypeRef\nexport function carriesUnknown(ref: TypeRef): boolean {\n if (ref.kind === 'primitive') return ref.primitive === 'unknown';\n if (ref.kind === 'array') return carriesUnknown(ref.items!);\n if (ref.kind === 'openMap') return carriesUnknown(ref.values!);\n return false;\n}\n\n/**\n * `void`, `unknown` and `any` all reach the model as the same primitive — the resolver maps the\n * three keywords onto one, because to a document they say the identical thing: nothing.\n */\n// webpieces-disable no-function-outside-class -- pure predicate over one TypeRef\nexport function isVoidLike(ref: TypeRef): boolean {\n return ref.kind === 'primitive' && ref.primitive === 'unknown';\n}\n\n/**\n * Every reason this tool could not be served, in the order `McpToolRegistry` asks them at boot —\n * unique name, trigger kind, HTTP auth, MCP auth — and then the renderer's own verdict.\n *\n * The registry's four are restated from the model rather than driven, because they are decorator\n * PRESENCE facts the model already carries verbatim; the SCHEMA question is the one with a real\n * implementation behind it, and that one is driven.\n */\n// webpieces-disable no-function-outside-class, max-lines-new-methods -- pure judge over one endpoint, and the four boot checks read as one list\nexport function toolFailures(\n endpoint: DocumentedEndpoint,\n renderer: McpSchemaRenderer,\n toolNames: Map<string, string>,\n model: ApiDocModel,\n): readonly Verdict[] {\n const found: Verdict[] = [];\n const toolName = endpoint.mcpTool!.name;\n const claimed = toolNames.get(toolName);\n if (claimed === undefined) {\n toolNames.set(toolName, `${model.contractName}.${endpoint.methodName}`);\n } else {\n found.push(\n new Verdict(\n `tool name '${toolName}' is already declared by ${claimed}`,\n 'Tool names are the protocol identity and are globally unique — rename one of the ' +\n 'two @WpMcpTool declarations.',\n ),\n );\n }\n if (endpoint.kind !== RPC_KIND) {\n found.push(\n new Verdict(\n `an MCP tool is declared on a '${endpoint.kind}' endpoint`,\n 'Only an RPC endpoint can be a tool — a cloudtasks, cron or external endpoint is ' +\n 'driven by something other than a caller. Make it RPC, or drop the @WpMcpTool.',\n ),\n );\n }\n if (endpoint.auth === undefined) {\n found.push(\n new Verdict(\n 'an MCP tool declares no HTTP auth',\n 'Put a @WpAuth... decorator on the method. The MCP server refuses to boot without ' +\n 'one, because a tool with no credential is an unauthenticated endpoint an ' +\n 'agent can call.',\n ),\n );\n }\n if (endpoint.mcpAuthText === undefined) {\n found.push(\n new Verdict(\n 'an MCP tool does not declare @WpMcpAuthJwt(...)',\n 'Add @WpMcpAuthJwt(...) beside the HTTP auth. MCP authorization is rechecked ' +\n 'before the endpoint boundary and is declared separately on purpose.',\n ),\n );\n }\n const rendered = renderFailure(renderer, endpoint);\n if (rendered !== undefined) found.push(rendered);\n return found;\n}\n\n/**\n * The renderer's own refusal for one tool, or undefined when it renders.\n *\n * This is the whole acceptance contract for the MCP half: \"passes the rule\" and \"renders a tool\" are\n * the SAME statement, because the rule asks the renderer. It covers the method's missing JSDoc,\n * every reachable DTO field's missing JSDoc, a request or response that is not a named object, a\n * root-level union, a recursive DTO, an `unknown` leaf and a malformed `@mcpHeader` — none of which\n * is restated here.\n *\n * One verdict per tool, because the renderer stops at the first thing it cannot draw. A tool blocked\n * by several undocumented fields is therefore fixed a round at a time, which is the price of having\n * exactly one definition of renderable rather than a second walk that could disagree with it.\n */\n// webpieces-disable no-function-outside-class -- pure judge beside toolFailures\nfunction renderFailure(\n renderer: McpSchemaRenderer,\n endpoint: DocumentedEndpoint,\n): Verdict | undefined {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- the render refusal IS the verdict\n try {\n renderer.tool(endpoint);\n return undefined;\n } catch (err: unknown) {\n //const error = toError(err);\n if (!(err instanceof McpRenderError)) throw err;\n return new Verdict(`${err.message} — no MCP schema could be built`, err.cure);\n }\n}\n\n/** Every `@ApiPath` class name declared at the top level of a file, in declaration order. */\n// webpieces-disable no-function-outside-class -- pure AST reader\nexport function contractNamesIn(source: ts.SourceFile): string[] {\n const names: string[] = [];\n for (const statement of source.statements) {\n if (!ts.isClassDeclaration(statement) || statement.name === undefined) continue;\n if (hasDecoratorNamed(statement, 'ApiPath')) names.push(statement.name.text);\n }\n return names;\n}\n\n/**\n * Where each method of one contract starts, and which of them carry `@WpMcpTool` without\n * `@Endpoint`.\n *\n * The line numbers exist because the MCP failures come from the RENDERER, whose location is a NAME\n * (`Order.window`) rather than a file offset — it publishes a schema, not a diagnostic. Anchoring\n * every tool failure at its METHOD is also where an author would write the disable: \"this tool is\n * deliberately not renderable\" is a decision about the method, not about a field three DTOs down.\n */\n// webpieces-disable no-function-outside-class -- pure AST reader beside contractNamesIn\nexport function contractLinesOf(source: ts.SourceFile, contractName: string): ContractLines {\n for (const statement of source.statements) {\n if (!ts.isClassDeclaration(statement) || statement.name?.text !== contractName) continue;\n const byMethod = new Map<string, number>();\n const orphanTools: string[] = [];\n for (const member of statement.members) {\n if (!ts.isMethodDeclaration(member) || !ts.isIdentifier(member.name)) continue;\n byMethod.set(member.name.text, lineOf(source, member));\n if (hasDecoratorNamed(member, 'WpMcpTool') && !hasDecoratorNamed(member, 'Endpoint')) {\n orphanTools.push(member.name.text);\n }\n }\n return new ContractLines(lineOf(source, statement), byMethod, orphanTools);\n }\n return new ContractLines(1, new Map<string, number>(), []);\n}\n\n/** 1-based line of a node's first token, decorators excluded. */\n// webpieces-disable no-function-outside-class -- pure AST reader\nfunction lineOf(source: ts.SourceFile, node: ts.Node): number {\n return source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1;\n}\n\n/** True when `node` carries `@name(...)`. Matched on the syntax, like every reader in this file. */\n// webpieces-disable no-function-outside-class -- pure AST reader\nfunction hasDecoratorNamed(node: ts.Node, name: string): boolean {\n const decorators = ts.canHaveDecorators(node) ? (ts.getDecorators(node) ?? []) : [];\n for (const decorator of decorators) {\n const call = decorator.expression;\n if (\n ts.isCallExpression(call) &&\n ts.isIdentifier(call.expression) &&\n call.expression.text === name\n ) {\n return true;\n }\n }\n return false;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"api-doc-rules-verdicts.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-doc-rules-verdicts.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The pure VERDICTS the api-contract rules print, split out of `api-doc-rules-scan.ts`, which owns\n * the walk and is at its file-size limit.\n *\n * Nothing here touches the filesystem or the project graph. Each function answers one question about\n * ONE declaration the extractor or the renderer has already ruled on, and turns it into the two\n * halves every webpieces refusal carries — what is wrong, and what to do instead. The DECISION is\n * never made here: `model.unmapped` is the extractor's and a render failure is the renderer's. What\n * this file adds is the WORDING, which is the part a generic \"no model representation for this type\n * form\" cannot give an author.\n */\n\nimport * as ts from 'typescript';\nimport {\n ApiDocModel,\n DocumentedEndpoint,\n McpRenderError,\n McpSchemaRenderer,\n TypeRef,\n UnmappedType,\n} from '@webpieces/api-doc-model';\nimport { EndpointKind } from './api-relations';\n\n/**\n * The one trigger kind an MCP tool may have. A LITERAL, for the same reason `EndpointKind` beside it\n * is one: this package deliberately does not depend on `@webpieces/core-util` at runtime, and the\n * set the literal belongs to is pinned by that type.\n */\nexport const RPC_KIND: EndpointKind = 'rpc';\n\n/**\n * The endpoint kinds whose response a caller WAITS FOR, and which therefore must name a DTO (#1017).\n *\n * `rpc` and `external` are both synchronous request/response: somebody posts and reads what comes\n * back, so there IS a body and it must be a named type that can gain a field later. `void` on either\n * is a one-way door — a `void` response can never grow without breaking every generated client, where\n * an empty DTO grows additively forever. #1016 refused it on `rpc` only, because `external` was\n * unstated at the time; it is stated now.\n *\n * `cloudtasks` and `cron` are deliberately NOT here: delivery is fire-and-forget, nobody waits for a\n * body, and `Promise<void>` is the CONTRACT rather than an omission.\n */\nexport const ANSWERING_KINDS: readonly EndpointKind[] = ['rpc', 'external'];\n\n/** The scalar keywords a MIXED SCALAR union is made of. */\nconst SCALAR_KEYWORDS = new Set<ts.SyntaxKind>([\n ts.SyntaxKind.StringKeyword,\n ts.SyntaxKind.NumberKeyword,\n ts.SyntaxKind.BooleanKeyword,\n]);\n\n/** ONE reason a declaration is refused, in the two halves every refusal prints. */\nexport class Verdict {\n constructor(\n public readonly what: string,\n public readonly cure: string,\n ) {}\n}\n\n\n/** Method name -> the line its declaration starts on, for one contract class. */\nexport class ContractLines {\n constructor(\n public readonly classLine: number,\n public readonly byMethod: ReadonlyMap<string, number>,\n /** Methods carrying `@WpMcpTool` that do NOT also carry `@Endpoint`. */\n public readonly toolsWithoutEndpoint: readonly string[],\n ) {}\n\n lineOf(methodName: string): number {\n return this.byMethod.get(methodName) ?? this.classLine;\n }\n}\n\n\n/**\n * The cure for an `unknown` value, for whichever rule is REPORTING it.\n *\n * It takes the rule name rather than naming one, because this defect is shared: it blocks the\n * OpenAPI document, so `DefectSink` routes it to `api-rules-for-openapi` when that rule is running\n * and to `api-rules-for-mcp` when it is the only one. A hard-coded rule name would then hand an\n * author, on the mcp-only configuration, a `// webpieces-disable api-rules-for-openapi` line that\n * the collector reading the site does not look for — a cure that does not cure, which is the one\n * failure mode a printed cure must not have.\n */\n// webpieces-disable no-function-outside-class -- pure message builder, beside the verdicts that print it\nexport function unknownValueCure(rule: string): string {\n return (\n 'Name the value type — a DTO, an array of one, a string-literal union, or ' +\n 'Record<string, ThatDto>. If the shape really is unknowable (a transport envelope, ' +\n 'arbitrary SQL rows), write the per-site disable and say so: ' +\n `// webpieces-disable ${rule} -- <why this field is published with no shape>. ` +\n 'An existing no-any-unknown disable does NOT count — that rule asked whether the code is ' +\n 'type-safe, which is a different question from whether a partner is handed a shapeless field.'\n );\n}\n\n/** The one cure for a `void` endpoint that somebody is WAITING on (rpc or external). */\nexport const VOID_RPC_CURE =\n 'Declare a named response DTO and return Promise<That>, even when it has no fields today: an ' +\n 'empty object grows additively forever, and a void response cannot gain a field without ' +\n 'breaking every generated client. An external endpoint is a synchronous call an outside caller ' +\n 'waits on, so it has a body too — an empty one serializes as {} and the failure detail rides on ' +\n 'the thrown error. Promise<void> stays legal on a cloudtasks or cron endpoint, where ' +\n 'fire-and-forget is the contract.';\n\n/**\n * Why the resolver could not map this type, said in the author's vocabulary.\n *\n * The DECISION to refuse is the extractor's and is not second-guessed here — every entry in\n * `model.unmapped` becomes a defect. What this adds is the MESSAGE: an open enum and a mixed scalar\n * union are the two shapes a real repo actually hits (measured: 6 of 98 endpoints, from exactly\n * these two), and \"no model representation for this type form\" tells their author nothing.\n */\n// webpieces-disable no-function-outside-class -- pure classifier over the extractor's own verdict\nexport function classifyUnmapped(unmapped: UnmappedType): Verdict {\n const branches = unionBranchesOf(unmapped.typeText);\n if (isOpenEnum(branches)) {\n return new Verdict(\n `'${unmapped.typeText}' is an OPEN ENUM — literals unioned with the wide type`,\n 'Use an enum OR a string, not both. TypeScript COLLAPSES this union to the wide type, ' +\n 'so the literals buy no compile-time safety and a document cannot state them as a ' +\n \"closed set. Either drop the `| string` (`'a' | 'b'` publishes as a real enum), or \" +\n 'drop the literals and list the known values in the JSDoc.',\n );\n }\n if (isMixedScalarUnion(branches)) {\n return new Verdict(\n `'${unmapped.typeText}' is a MIXED SCALAR union`,\n 'Give the field ONE type. JSON Schema can spell {\"type\": [\"string\",\"number\"]}, but ' +\n 'this framework\\'s ApiJsonSchema helpers read a type array as \"one real type plus ' +\n 'maybe null\" (baseTypeOf returns the first non-null member), so publishing it ' +\n 'would VALIDATE WRONGLY — which is worse than refusing it. Pick the type, or wrap ' +\n 'the alternatives in a discriminated union of named objects.',\n );\n }\n return new Verdict(\n `'${unmapped.typeText}' has no shape a document can state — ${unmapped.reason}`,\n 'Give it a type a document can carry: a named DTO, an array of one, a string-literal ' +\n 'union, a Record<string, ThatDto>, or a primitive. A field with no shape publishes as ' +\n '\"anything\".',\n );\n}\n\n/** The branches of `typeText` when it parses as a union, else an empty list. */\n// webpieces-disable no-function-outside-class -- pure parser used by classifyUnmapped alone\nfunction unionBranchesOf(typeText: string): readonly ts.TypeNode[] {\n const parsed = ts.createSourceFile(\n '__unmapped.ts',\n `type __X = ${typeText};`,\n ts.ScriptTarget.Latest,\n true,\n );\n const alias = parsed.statements[0];\n if (alias === undefined || !ts.isTypeAliasDeclaration(alias)) return [];\n if (!ts.isUnionTypeNode(alias.type)) return [];\n return alias.type.types.filter((branch: ts.TypeNode) => !isNullishBranch(branch));\n}\n\n/** `'a' | 'b' | string`, and the numeric equivalent. See #1010 and the decision on #1011. */\n// webpieces-disable no-function-outside-class -- pure predicate beside classifyUnmapped\nfunction isOpenEnum(branches: readonly ts.TypeNode[]): boolean {\n const wide = branches.some(\n (branch: ts.TypeNode) =>\n branch.kind === ts.SyntaxKind.StringKeyword ||\n branch.kind === ts.SyntaxKind.NumberKeyword,\n );\n const literals = branches.some((branch: ts.TypeNode) => ts.isLiteralTypeNode(branch));\n return wide && literals;\n}\n\n/** `string | number`, `string | boolean | null` — two or more DIFFERENT scalar keywords. */\n// webpieces-disable no-function-outside-class -- pure predicate beside classifyUnmapped\nfunction isMixedScalarUnion(branches: readonly ts.TypeNode[]): boolean {\n if (branches.length < 2) return false;\n const kinds = new Set<ts.SyntaxKind>();\n for (const branch of branches) {\n if (!SCALAR_KEYWORDS.has(branch.kind)) return false;\n kinds.add(branch.kind);\n }\n return kinds.size >= 2;\n}\n\n/** `null` / `undefined` branches — nullability, not composition, exactly as the resolver reads it. */\n// webpieces-disable no-function-outside-class -- pure predicate beside classifyUnmapped\nfunction isNullishBranch(node: ts.TypeNode): boolean {\n if (node.kind === ts.SyntaxKind.UndefinedKeyword || node.kind === ts.SyntaxKind.NullKeyword) {\n return true;\n }\n return ts.isLiteralTypeNode(node) && node.literal.kind === ts.SyntaxKind.NullKeyword;\n}\n\n/**\n * True when this type PUBLISHES an `unknown` value.\n *\n * It looks at the VALUE TYPE wherever it sits — the item of an array, the value of an open map, the\n * field itself — and never at the `Record<>` spelling, because the real cases in the wild are not\n * all Records: `params?: unknown[]` is the same defect written differently.\n *\n * It does NOT descend through a `ref`: a named DTO is its own entry in the model and its own fields\n * are judged there, so descending would report the same field once per type that points at it.\n */\n// webpieces-disable no-function-outside-class -- pure predicate over one TypeRef\nexport function carriesUnknown(ref: TypeRef): boolean {\n if (ref.kind === 'primitive') return ref.primitive === 'unknown';\n if (ref.kind === 'array') return carriesUnknown(ref.items!);\n if (ref.kind === 'openMap') return carriesUnknown(ref.values!);\n return false;\n}\n\n/**\n * `void`, `unknown` and `any` all reach the model as the same primitive — the resolver maps the\n * three keywords onto one, because to a document they say the identical thing: nothing.\n */\n// webpieces-disable no-function-outside-class -- pure predicate over one TypeRef\nexport function isVoidLike(ref: TypeRef): boolean {\n return ref.kind === 'primitive' && ref.primitive === 'unknown';\n}\n\n/**\n * Every reason this tool could not be served, in the order `McpToolRegistry` asks them at boot —\n * unique name, trigger kind, HTTP auth, MCP auth — and then the renderer's own verdict.\n *\n * The registry's four are restated from the model rather than driven, because they are decorator\n * PRESENCE facts the model already carries verbatim; the SCHEMA question is the one with a real\n * implementation behind it, and that one is driven.\n */\n// webpieces-disable no-function-outside-class, max-lines-new-methods -- pure judge over one endpoint, and the four boot checks read as one list\nexport function toolFailures(\n endpoint: DocumentedEndpoint,\n renderer: McpSchemaRenderer,\n toolNames: Map<string, string>,\n model: ApiDocModel,\n): readonly Verdict[] {\n const found: Verdict[] = [];\n const toolName = endpoint.mcpTool!.name;\n const claimed = toolNames.get(toolName);\n if (claimed === undefined) {\n toolNames.set(toolName, `${model.contractName}.${endpoint.methodName}`);\n } else {\n found.push(\n new Verdict(\n `tool name '${toolName}' is already declared by ${claimed}`,\n 'Tool names are the protocol identity and are globally unique — rename one of the ' +\n 'two @WpMcpTool declarations.',\n ),\n );\n }\n if (endpoint.kind !== RPC_KIND) {\n found.push(\n new Verdict(\n `an MCP tool is declared on a '${endpoint.kind}' endpoint`,\n 'Only an RPC endpoint can be a tool — a cloudtasks, cron or external endpoint is ' +\n 'driven by something other than a caller. Make it RPC, or drop the @WpMcpTool.',\n ),\n );\n }\n if (endpoint.auth === undefined) {\n found.push(\n new Verdict(\n 'an MCP tool declares no HTTP auth',\n 'Put a @WpAuth... decorator on the method. The MCP server refuses to boot without ' +\n 'one, because a tool with no credential is an unauthenticated endpoint an ' +\n 'agent can call.',\n ),\n );\n }\n if (endpoint.mcpAuthText === undefined) {\n found.push(\n new Verdict(\n 'an MCP tool does not declare @WpMcpAuthJwt(...)',\n 'Add @WpMcpAuthJwt(...) beside the HTTP auth. MCP authorization is rechecked ' +\n 'before the endpoint boundary and is declared separately on purpose.',\n ),\n );\n }\n const rendered = renderFailure(renderer, endpoint);\n if (rendered !== undefined) found.push(rendered);\n return found;\n}\n\n/**\n * The renderer's own refusal for one tool, or undefined when it renders.\n *\n * This is the whole acceptance contract for the MCP half: \"passes the rule\" and \"renders a tool\" are\n * the SAME statement, because the rule asks the renderer. It covers the method's missing JSDoc,\n * every reachable DTO field's missing JSDoc, a request or response that is not a named object, a\n * root-level union, a recursive DTO, an `unknown` leaf and a malformed `@mcpHeader` — none of which\n * is restated here.\n *\n * One verdict per tool, because the renderer stops at the first thing it cannot draw. A tool blocked\n * by several undocumented fields is therefore fixed a round at a time, which is the price of having\n * exactly one definition of renderable rather than a second walk that could disagree with it.\n */\n// webpieces-disable no-function-outside-class -- pure judge beside toolFailures\nfunction renderFailure(\n renderer: McpSchemaRenderer,\n endpoint: DocumentedEndpoint,\n): Verdict | undefined {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- the render refusal IS the verdict\n try {\n renderer.tool(endpoint);\n return undefined;\n } catch (err: unknown) {\n //const error = toError(err);\n if (!(err instanceof McpRenderError)) throw err;\n return new Verdict(`${err.message} — no MCP schema could be built`, err.cure);\n }\n}\n\n/** Every `@ApiPath` class name declared at the top level of a file, in declaration order. */\n// webpieces-disable no-function-outside-class -- pure AST reader\nexport function contractNamesIn(source: ts.SourceFile): string[] {\n const names: string[] = [];\n for (const statement of source.statements) {\n if (!ts.isClassDeclaration(statement) || statement.name === undefined) continue;\n if (hasDecoratorNamed(statement, 'ApiPath')) names.push(statement.name.text);\n }\n return names;\n}\n\n/**\n * Where each method of one contract starts, and which of them carry `@WpMcpTool` without\n * `@Endpoint`.\n *\n * The line numbers exist because the MCP failures come from the RENDERER, whose location is a NAME\n * (`Order.window`) rather than a file offset — it publishes a schema, not a diagnostic. Anchoring\n * every tool failure at its METHOD is also where an author would write the disable: \"this tool is\n * deliberately not renderable\" is a decision about the method, not about a field three DTOs down.\n */\n// webpieces-disable no-function-outside-class -- pure AST reader beside contractNamesIn\nexport function contractLinesOf(source: ts.SourceFile, contractName: string): ContractLines {\n for (const statement of source.statements) {\n if (!ts.isClassDeclaration(statement) || statement.name?.text !== contractName) continue;\n const byMethod = new Map<string, number>();\n const orphanTools: string[] = [];\n for (const member of statement.members) {\n if (!ts.isMethodDeclaration(member) || !ts.isIdentifier(member.name)) continue;\n byMethod.set(member.name.text, lineOf(source, member));\n if (hasDecoratorNamed(member, 'WpMcpTool') && !hasDecoratorNamed(member, 'Endpoint')) {\n orphanTools.push(member.name.text);\n }\n }\n return new ContractLines(lineOf(source, statement), byMethod, orphanTools);\n }\n return new ContractLines(1, new Map<string, number>(), []);\n}\n\n/** 1-based line of a node's first token, decorators excluded. */\n// webpieces-disable no-function-outside-class -- pure AST reader\nfunction lineOf(source: ts.SourceFile, node: ts.Node): number {\n return source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1;\n}\n\n/** True when `node` carries `@name(...)`. Matched on the syntax, like every reader in this file. */\n// webpieces-disable no-function-outside-class -- pure AST reader\nfunction hasDecoratorNamed(node: ts.Node, name: string): boolean {\n const decorators = ts.canHaveDecorators(node) ? (ts.getDecorators(node) ?? []) : [];\n for (const decorator of decorators) {\n const call = decorator.expression;\n if (\n ts.isCallExpression(call) &&\n ts.isIdentifier(call.expression) &&\n call.expression.text === name\n ) {\n return true;\n }\n }\n return false;\n}\n"]}
|
|
@@ -99,26 +99,63 @@ export declare class ApiDocRulesFindings {
|
|
|
99
99
|
mcpExclusions?: readonly McpExclusion[]);
|
|
100
100
|
static empty(): ApiDocRulesFindings;
|
|
101
101
|
}
|
|
102
|
+
/** The mode value that narrows the scan to the projects the diff touched. */
|
|
103
|
+
export declare const AFFECTED_PROJECT_MODE = "AFFECTED_PROJECT";
|
|
102
104
|
/**
|
|
103
105
|
* ONE rule's switches, resolved from webpieces.config.json once per scan.
|
|
104
106
|
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
107
|
+
* There is NO default (#1017). Both rules are schema'd, so `webpieces.config.json` must carry an
|
|
108
|
+
* entry for each or the config FAILS TO LOAD naming them — a consumer states `OFF`,
|
|
109
|
+
* `AFFECTED_PROJECT` or `RUN_EVERY_TIME` out loud. The `off()` a bare constructor or a unit test
|
|
110
|
+
* gets is not a default; it is what a caller that configured nothing asked for.
|
|
108
111
|
*/
|
|
109
112
|
export declare class ApiDocRule {
|
|
110
113
|
readonly name: string;
|
|
111
114
|
readonly enabled: boolean;
|
|
112
115
|
/** Project roots this rule does not apply to — `allowedPaths` in the config. */
|
|
113
116
|
readonly allowedPaths: readonly string[];
|
|
117
|
+
/**
|
|
118
|
+
* Workspace-relative paths the diff touched, under `AFFECTED_PROJECT`; `null` under
|
|
119
|
+
* `RUN_EVERY_TIME`, which means every project is in scope.
|
|
120
|
+
*
|
|
121
|
+
* `null` is also what an UNCOMPUTABLE diff resolves to — no merge-base, a shallow clone, no
|
|
122
|
+
* repository at all. A diff that could not be read is not evidence that nothing changed, and
|
|
123
|
+
* the only safe reading of "I do not know what changed" is "look at all of it".
|
|
124
|
+
*/
|
|
125
|
+
readonly changedPaths: readonly string[] | null;
|
|
114
126
|
constructor(name: string, enabled: boolean,
|
|
115
127
|
/** Project roots this rule does not apply to — `allowedPaths` in the config. */
|
|
116
|
-
allowedPaths: readonly string[]
|
|
117
|
-
/**
|
|
128
|
+
allowedPaths: readonly string[],
|
|
129
|
+
/**
|
|
130
|
+
* Workspace-relative paths the diff touched, under `AFFECTED_PROJECT`; `null` under
|
|
131
|
+
* `RUN_EVERY_TIME`, which means every project is in scope.
|
|
132
|
+
*
|
|
133
|
+
* `null` is also what an UNCOMPUTABLE diff resolves to — no merge-base, a shallow clone, no
|
|
134
|
+
* repository at all. A diff that could not be read is not evidence that nothing changed, and
|
|
135
|
+
* the only safe reading of "I do not know what changed" is "look at all of it".
|
|
136
|
+
*/
|
|
137
|
+
changedPaths?: readonly string[] | null);
|
|
138
|
+
/** ARMED over every project — what a unit test constructs, and what RUN_EVERY_TIME resolves to. */
|
|
118
139
|
static armed(name: string): ApiDocRule;
|
|
140
|
+
/** ARMED over the projects owning one of `changedPaths` — what AFFECTED_PROJECT resolves to. */
|
|
141
|
+
static affected(name: string, changedPaths: readonly string[]): ApiDocRule;
|
|
119
142
|
static off(name: string): ApiDocRule;
|
|
143
|
+
/**
|
|
144
|
+
* True when this rule scans the contracts of the project rooted at `root`.
|
|
145
|
+
*
|
|
146
|
+
* ONE place answers it, for both rules, so `allowedPaths` and the affected-project narrowing can
|
|
147
|
+
* never be applied by one caller and skipped by another.
|
|
148
|
+
*/
|
|
149
|
+
coversProject(root: string): boolean;
|
|
120
150
|
/** `mode: OFF` and the time-box/branch hatches come from RuleGate, so there is ONE reading. */
|
|
121
151
|
static fromConfig(workspaceRoot: string, name: string): ApiDocRule;
|
|
152
|
+
/**
|
|
153
|
+
* Every path the diff touched, against the base nx itself uses (`NX_BASE`, else the merge-base
|
|
154
|
+
* with origin/main). NOT ts-only and INCLUDING deletions: a deleted DTO changes what a project's
|
|
155
|
+
* contracts can express exactly as an added one does, and a `project.json` edit can change which
|
|
156
|
+
* project owns a contract at all.
|
|
157
|
+
*/
|
|
158
|
+
private static changedPathsOf;
|
|
122
159
|
}
|
|
123
160
|
/** ONE `webpieces-disable` naming a rule, and whether it gave a reason. */
|
|
124
161
|
export declare class DisableComment {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* config is read exactly once per executor run.
|
|
8
8
|
*/
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
-
exports.DisableComment = exports.ApiDocRule = exports.ApiDocRulesFindings = exports.McpExclusion = exports.ApiRuleFindings = exports.ApiContractDefect = exports.EXTERNAL_CUSTOMER_API_TYPE = exports.MCP_RULE = exports.OPENAPI_RULE = void 0;
|
|
10
|
+
exports.DisableComment = exports.ApiDocRule = exports.AFFECTED_PROJECT_MODE = exports.ApiDocRulesFindings = exports.McpExclusion = exports.ApiRuleFindings = exports.ApiContractDefect = exports.EXTERNAL_CUSTOMER_API_TYPE = exports.MCP_RULE = exports.OPENAPI_RULE = void 0;
|
|
11
11
|
const tslib_1 = require("tslib");
|
|
12
12
|
const fs = tslib_1.__importStar(require("fs"));
|
|
13
13
|
const rules_config_1 = require("@webpieces/rules-config");
|
|
@@ -131,32 +131,67 @@ class ApiDocRulesFindings {
|
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
exports.ApiDocRulesFindings = ApiDocRulesFindings;
|
|
134
|
+
/** The mode value that narrows the scan to the projects the diff touched. */
|
|
135
|
+
exports.AFFECTED_PROJECT_MODE = 'AFFECTED_PROJECT';
|
|
134
136
|
/**
|
|
135
137
|
* ONE rule's switches, resolved from webpieces.config.json once per scan.
|
|
136
138
|
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
139
|
+
* There is NO default (#1017). Both rules are schema'd, so `webpieces.config.json` must carry an
|
|
140
|
+
* entry for each or the config FAILS TO LOAD naming them — a consumer states `OFF`,
|
|
141
|
+
* `AFFECTED_PROJECT` or `RUN_EVERY_TIME` out loud. The `off()` a bare constructor or a unit test
|
|
142
|
+
* gets is not a default; it is what a caller that configured nothing asked for.
|
|
140
143
|
*/
|
|
141
144
|
class ApiDocRule {
|
|
142
145
|
name;
|
|
143
146
|
enabled;
|
|
144
147
|
allowedPaths;
|
|
148
|
+
changedPaths;
|
|
145
149
|
constructor(name, enabled,
|
|
146
150
|
/** Project roots this rule does not apply to — `allowedPaths` in the config. */
|
|
147
|
-
allowedPaths
|
|
151
|
+
allowedPaths,
|
|
152
|
+
/**
|
|
153
|
+
* Workspace-relative paths the diff touched, under `AFFECTED_PROJECT`; `null` under
|
|
154
|
+
* `RUN_EVERY_TIME`, which means every project is in scope.
|
|
155
|
+
*
|
|
156
|
+
* `null` is also what an UNCOMPUTABLE diff resolves to — no merge-base, a shallow clone, no
|
|
157
|
+
* repository at all. A diff that could not be read is not evidence that nothing changed, and
|
|
158
|
+
* the only safe reading of "I do not know what changed" is "look at all of it".
|
|
159
|
+
*/
|
|
160
|
+
changedPaths = null) {
|
|
148
161
|
this.name = name;
|
|
149
162
|
this.enabled = enabled;
|
|
150
163
|
this.allowedPaths = allowedPaths;
|
|
164
|
+
this.changedPaths = changedPaths;
|
|
151
165
|
}
|
|
152
|
-
/** ARMED
|
|
166
|
+
/** ARMED over every project — what a unit test constructs, and what RUN_EVERY_TIME resolves to. */
|
|
153
167
|
// webpieces-disable no-function-outside-class -- static factory of this class
|
|
154
168
|
static armed(name) {
|
|
155
|
-
return new ApiDocRule(name, true, []);
|
|
169
|
+
return new ApiDocRule(name, true, [], null);
|
|
170
|
+
}
|
|
171
|
+
/** ARMED over the projects owning one of `changedPaths` — what AFFECTED_PROJECT resolves to. */
|
|
172
|
+
// webpieces-disable no-function-outside-class -- static factory of this class
|
|
173
|
+
static affected(name, changedPaths) {
|
|
174
|
+
return new ApiDocRule(name, true, [], changedPaths);
|
|
156
175
|
}
|
|
157
176
|
// webpieces-disable no-function-outside-class -- static factory of this class
|
|
158
177
|
static off(name) {
|
|
159
|
-
return new ApiDocRule(name, false, []);
|
|
178
|
+
return new ApiDocRule(name, false, [], null);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* True when this rule scans the contracts of the project rooted at `root`.
|
|
182
|
+
*
|
|
183
|
+
* ONE place answers it, for both rules, so `allowedPaths` and the affected-project narrowing can
|
|
184
|
+
* never be applied by one caller and skipped by another.
|
|
185
|
+
*/
|
|
186
|
+
coversProject(root) {
|
|
187
|
+
if (!this.enabled)
|
|
188
|
+
return false;
|
|
189
|
+
if ((0, rules_config_1.matchesAnyGlob)(root, this.allowedPaths))
|
|
190
|
+
return false;
|
|
191
|
+
if (this.changedPaths === null)
|
|
192
|
+
return true;
|
|
193
|
+
const prefix = `${root}/`;
|
|
194
|
+
return this.changedPaths.some((each) => each.startsWith(prefix));
|
|
160
195
|
}
|
|
161
196
|
/** `mode: OFF` and the time-box/branch hatches come from RuleGate, so there is ONE reading. */
|
|
162
197
|
// webpieces-disable no-function-outside-class -- static factory of this class
|
|
@@ -166,7 +201,28 @@ class ApiDocRule {
|
|
|
166
201
|
}
|
|
167
202
|
const rule = (0, rules_config_1.loadAndValidate)(workspaceRoot).resolved.rules.get(name);
|
|
168
203
|
const allowed = rule?.options['allowedPaths'];
|
|
169
|
-
|
|
204
|
+
const allowedPaths = Array.isArray(allowed) ? allowed : [];
|
|
205
|
+
const affected = rule?.options['mode'] === exports.AFFECTED_PROJECT_MODE
|
|
206
|
+
? ApiDocRule.changedPathsOf(workspaceRoot)
|
|
207
|
+
: null;
|
|
208
|
+
return new ApiDocRule(name, true, allowedPaths, affected);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Every path the diff touched, against the base nx itself uses (`NX_BASE`, else the merge-base
|
|
212
|
+
* with origin/main). NOT ts-only and INCLUDING deletions: a deleted DTO changes what a project's
|
|
213
|
+
* contracts can express exactly as an added one does, and a `project.json` edit can change which
|
|
214
|
+
* project owns a contract at all.
|
|
215
|
+
*/
|
|
216
|
+
// webpieces-disable no-function-outside-class -- private static reader of this class
|
|
217
|
+
static changedPathsOf(workspaceRoot) {
|
|
218
|
+
const scope = new rules_config_1.DiffScope();
|
|
219
|
+
const range = scope.resolveBase(workspaceRoot);
|
|
220
|
+
if (range.base === undefined)
|
|
221
|
+
return null;
|
|
222
|
+
const opts = new rules_config_1.ChangedFilesOptions();
|
|
223
|
+
opts.tsOnly = false;
|
|
224
|
+
opts.includeDeletions = true;
|
|
225
|
+
return scope.getChangedFiles(workspaceRoot, range.base, range.head, opts);
|
|
170
226
|
}
|
|
171
227
|
}
|
|
172
228
|
exports.ApiDocRule = ApiDocRule;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-doc-rules.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-doc-rules.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The DATA and the SWITCHES of `api-rules-for-openapi` and `api-rules-for-mcp` (#1011).\n *\n * The scan itself is in `api-doc-rules-scan.ts`; this file holds what a caller reads back and the\n * per-rule config, so a unit test can construct either without touching a config file and so the\n * config is read exactly once per executor run.\n */\n\nimport * as fs from 'fs';\nimport { loadAndValidate, RULE_NAMES, WEBPIECES_DISABLE } from '@webpieces/rules-config';\nimport { RuleGate } from '../rule-gate';\n\n/** As written in a disable comment and as a config key. */\nexport const OPENAPI_RULE = RULE_NAMES.API_RULES_FOR_OPENAPI;\nexport const MCP_RULE = RULE_NAMES.API_RULES_FOR_MCP;\n\n/** The `@ApiType` value that means \"a partner reads this document\". */\nexport const EXTERNAL_CUSTOMER_API_TYPE = 'external-customer';\n\n/**\n * `// webpieces-disable <rule>[, <rule2>] -- <reason>`, with the reason CAPTURED so a reasonless\n * disable can be told apart from an absent one. Identical to the spelling `root-union-scan` reads.\n */\nconst DISABLE_RE = new RegExp(\n `//\\\\s*${WEBPIECES_DISABLE}\\\\s+([\\\\w-]+(?:\\\\s*,\\\\s*[\\\\w-]+)*)(?:\\\\s*--\\\\s*(.*))?$`,\n);\n\n/** How far above a declaration a disable comment may sit before it is somebody else's comment. */\nconst DISABLE_LOOKBACK_LINES = 40;\n\n/**\n * ONE defect on ONE contract.\n *\n * `exposure` is the contract's `@ApiType` list, and it is carried rather than derived because the\n * SAME defect is louder on a contract that reaches `external-customer`: an unshaped field on a\n * partner document costs a partner something, where the same field on a service-to-service contract\n * costs a colleague a question. The refusal sorts and labels on it.\n */\nexport class ApiContractDefect {\n constructor(\n /** The `@ApiPath` contract class. */\n public readonly api: string,\n /** The `@Endpoint` method, or `''` for a contract-level or DTO-level defect. */\n public readonly method: string,\n /** What is wrong, in one line, naming the declaration. */\n public readonly what: string,\n /** `path/to/File.ts:LINE`, workspace-relative. */\n public readonly at: string,\n /** What to do instead, in one sentence. */\n public readonly cure: string,\n /** The contract's `@ApiType` list — `svc-to-svc` when it declares none. */\n public readonly exposure: readonly string[],\n ) {}\n\n /** True when this contract feeds the PARTNER-facing document. */\n isExternal(): boolean {\n return this.exposure.includes(EXTERNAL_CUSTOMER_API_TYPE);\n }\n\n /** `Api.method` or `Api` — what a reader opens. */\n where(): string {\n return this.method === '' ? this.api : `${this.api}.${this.method}`;\n }\n}\n\n/** What ONE rule found: real defects, and disables of it that gave no reason. */\nexport class ApiRuleFindings {\n constructor(\n public readonly violations: readonly ApiContractDefect[],\n /**\n * Sites that named this rule in a disable and wrote no reason. A reasonless disable is\n * itself a violation: the point of the per-site hatch is the ARGUMENT, which is the only\n * part the next reader can weigh.\n */\n public readonly reasonlessDisables: readonly ApiContractDefect[],\n ) {}\n\n isEmpty(): boolean {\n return this.violations.length === 0 && this.reasonlessDisables.length === 0;\n }\n}\n\n/**\n * ONE endpoint declared PERMANENTLY outside MCP by `@InvalidEndpointForMcp('<reason>')` (#1014).\n *\n * Not a violation and not a suppression — a DECLARATION, which is why it is carried beside the\n * findings rather than in them. `api-rules-for-mcp` restates the whole list, with reasons, on every\n * run: an exclusion announced once at the moment somebody added it is an exclusion nobody will read\n * again, and a build-time warning at add time would be read exactly as little.\n */\nexport class McpExclusion {\n constructor(\n public readonly api: string,\n public readonly method: string,\n /** The decorator's reason, verbatim. Empty only when the argument could not be folded. */\n public readonly reason: string,\n /** `path/to/File.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/** Both rules' findings from ONE scan — one program build, two verdicts. */\nexport class ApiDocRulesFindings {\n constructor(\n public readonly openApi: ApiRuleFindings,\n public readonly mcp: ApiRuleFindings,\n /** Every `@InvalidEndpointForMcp` endpoint the MCP rule looked at, in declaration order. */\n public readonly mcpExclusions: readonly McpExclusion[] = [],\n ) {}\n\n // webpieces-disable no-function-outside-class -- static factory of this class\n static empty(): ApiDocRulesFindings {\n return new ApiDocRulesFindings(\n new ApiRuleFindings([], []),\n new ApiRuleFindings([], []),\n [],\n );\n }\n}\n\n/**\n * ONE rule's switches, resolved from webpieces.config.json once per scan.\n *\n * The DEFAULT is OFF, and unlike an absent entry elsewhere that is deliberate: `defaultRules` in\n * `@webpieces/rules-config` carries `mode: 'OFF'` for both, and the argument for it is written\n * there rather than here so there is one place to read it.\n */\nexport class ApiDocRule {\n constructor(\n public readonly name: string,\n public readonly enabled: boolean,\n /** Project roots this rule does not apply to — `allowedPaths` in the config. */\n public readonly allowedPaths: readonly string[],\n ) {}\n\n /** ARMED, everywhere — what a unit test constructs, and what an opted-in repo resolves to. */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static armed(name: string): ApiDocRule {\n return new ApiDocRule(name, true, []);\n }\n\n // webpieces-disable no-function-outside-class -- static factory of this class\n static off(name: string): ApiDocRule {\n return new ApiDocRule(name, false, []);\n }\n\n /** `mode: OFF` and the time-box/branch hatches come from RuleGate, so there is ONE reading. */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static fromConfig(workspaceRoot: string, name: string): ApiDocRule {\n if (new RuleGate().isDisabled(workspaceRoot, name, true)) {\n return ApiDocRule.off(name);\n }\n const rule = loadAndValidate(workspaceRoot).resolved.rules.get(name);\n const allowed = rule?.options['allowedPaths'];\n return new ApiDocRule(name, true, Array.isArray(allowed) ? (allowed as string[]) : []);\n }\n}\n\n/** ONE `webpieces-disable` naming a rule, and whether it gave a reason. */\nexport class DisableComment {\n constructor(public readonly hasReason: boolean) {}\n\n /**\n * The disable naming `rule` on the declaration at `line` (1-based) of `file`, or undefined.\n *\n * It walks UPWARD over the declaration's own leading trivia — blank lines, `//` comments, a\n * JSDoc block, and the decorators between them — because that is where an author writes one, and\n * it stops at the first line that is none of those, so a directive belonging to the PREVIOUS\n * declaration can never be read as covering this one.\n *\n * Only a directive NAMING this rule counts. An existing `// webpieces-disable no-any-unknown`\n * therefore does not silence these rules, which is the point: that rule answers \"is this\n * type-safe?\" and these answer \"is this field PUBLISHED with no shape?\" — different questions\n * with different right answers, so the second one wants its own, separately argued line.\n *\n * A file that cannot be read yields \"no disable\": a defect is still a defect, and inventing a\n * suppression out of an I/O failure is the one wrong answer.\n */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static readAt(file: string, line: number, rule: string): DisableComment | undefined {\n const lines = DisableComment.linesOf(file);\n if (lines === undefined) return undefined;\n const start = Math.max(0, line - 1);\n const onDeclaration = DisableComment.match(lines[start] ?? '', rule);\n if (onDeclaration !== undefined) return onDeclaration;\n for (let i = start - 1; i >= 0 && i >= start - DISABLE_LOOKBACK_LINES; i--) {\n const text = (lines[i] ?? '').trim();\n const above = DisableComment.match(text, rule);\n if (above !== undefined) return above;\n if (!DisableComment.isTrivia(text)) return undefined;\n }\n return undefined;\n }\n\n /** Leading trivia a disable comment is allowed to sit above: blanks, comments and decorators. */\n // webpieces-disable no-function-outside-class -- private static predicate of this class\n private static isTrivia(text: string): boolean {\n return (\n text === '' ||\n text.startsWith('//') ||\n text.startsWith('*') ||\n text.startsWith('/*') ||\n text.startsWith('@')\n );\n }\n\n // webpieces-disable no-function-outside-class -- private static reader of this class\n private static match(text: string, rule: string): DisableComment | undefined {\n const found = text.trim().match(DISABLE_RE);\n if (found === null) return undefined;\n const named = found[1].split(',').map((each: string): string => each.trim());\n if (!named.includes(rule)) return undefined;\n return new DisableComment((found[2] ?? '').trim() !== '');\n }\n\n /** The file's lines, or undefined when it cannot be read. */\n // webpieces-disable no-function-outside-class -- private static reader of this class\n private static linesOf(file: string): string[] | undefined {\n if (!fs.existsSync(file)) return undefined;\n return fs.readFileSync(file, 'utf8').split('\\n');\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"api-doc-rules.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-doc-rules.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The DATA and the SWITCHES of `api-rules-for-openapi` and `api-rules-for-mcp` (#1011).\n *\n * The scan itself is in `api-doc-rules-scan.ts`; this file holds what a caller reads back and the\n * per-rule config, so a unit test can construct either without touching a config file and so the\n * config is read exactly once per executor run.\n */\n\nimport * as fs from 'fs';\nimport {\n ChangedFilesOptions,\n DiffScope,\n loadAndValidate,\n matchesAnyGlob,\n RULE_NAMES,\n WEBPIECES_DISABLE,\n} from '@webpieces/rules-config';\nimport { RuleGate } from '../rule-gate';\n\n/** As written in a disable comment and as a config key. */\nexport const OPENAPI_RULE = RULE_NAMES.API_RULES_FOR_OPENAPI;\nexport const MCP_RULE = RULE_NAMES.API_RULES_FOR_MCP;\n\n/** The `@ApiType` value that means \"a partner reads this document\". */\nexport const EXTERNAL_CUSTOMER_API_TYPE = 'external-customer';\n\n/**\n * `// webpieces-disable <rule>[, <rule2>] -- <reason>`, with the reason CAPTURED so a reasonless\n * disable can be told apart from an absent one. Identical to the spelling `root-union-scan` reads.\n */\nconst DISABLE_RE = new RegExp(\n `//\\\\s*${WEBPIECES_DISABLE}\\\\s+([\\\\w-]+(?:\\\\s*,\\\\s*[\\\\w-]+)*)(?:\\\\s*--\\\\s*(.*))?$`,\n);\n\n/** How far above a declaration a disable comment may sit before it is somebody else's comment. */\nconst DISABLE_LOOKBACK_LINES = 40;\n\n/**\n * ONE defect on ONE contract.\n *\n * `exposure` is the contract's `@ApiType` list, and it is carried rather than derived because the\n * SAME defect is louder on a contract that reaches `external-customer`: an unshaped field on a\n * partner document costs a partner something, where the same field on a service-to-service contract\n * costs a colleague a question. The refusal sorts and labels on it.\n */\nexport class ApiContractDefect {\n constructor(\n /** The `@ApiPath` contract class. */\n public readonly api: string,\n /** The `@Endpoint` method, or `''` for a contract-level or DTO-level defect. */\n public readonly method: string,\n /** What is wrong, in one line, naming the declaration. */\n public readonly what: string,\n /** `path/to/File.ts:LINE`, workspace-relative. */\n public readonly at: string,\n /** What to do instead, in one sentence. */\n public readonly cure: string,\n /** The contract's `@ApiType` list — `svc-to-svc` when it declares none. */\n public readonly exposure: readonly string[],\n ) {}\n\n /** True when this contract feeds the PARTNER-facing document. */\n isExternal(): boolean {\n return this.exposure.includes(EXTERNAL_CUSTOMER_API_TYPE);\n }\n\n /** `Api.method` or `Api` — what a reader opens. */\n where(): string {\n return this.method === '' ? this.api : `${this.api}.${this.method}`;\n }\n}\n\n/** What ONE rule found: real defects, and disables of it that gave no reason. */\nexport class ApiRuleFindings {\n constructor(\n public readonly violations: readonly ApiContractDefect[],\n /**\n * Sites that named this rule in a disable and wrote no reason. A reasonless disable is\n * itself a violation: the point of the per-site hatch is the ARGUMENT, which is the only\n * part the next reader can weigh.\n */\n public readonly reasonlessDisables: readonly ApiContractDefect[],\n ) {}\n\n isEmpty(): boolean {\n return this.violations.length === 0 && this.reasonlessDisables.length === 0;\n }\n}\n\n/**\n * ONE endpoint declared PERMANENTLY outside MCP by `@InvalidEndpointForMcp('<reason>')` (#1014).\n *\n * Not a violation and not a suppression — a DECLARATION, which is why it is carried beside the\n * findings rather than in them. `api-rules-for-mcp` restates the whole list, with reasons, on every\n * run: an exclusion announced once at the moment somebody added it is an exclusion nobody will read\n * again, and a build-time warning at add time would be read exactly as little.\n */\nexport class McpExclusion {\n constructor(\n public readonly api: string,\n public readonly method: string,\n /** The decorator's reason, verbatim. Empty only when the argument could not be folded. */\n public readonly reason: string,\n /** `path/to/File.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/** Both rules' findings from ONE scan — one program build, two verdicts. */\nexport class ApiDocRulesFindings {\n constructor(\n public readonly openApi: ApiRuleFindings,\n public readonly mcp: ApiRuleFindings,\n /** Every `@InvalidEndpointForMcp` endpoint the MCP rule looked at, in declaration order. */\n public readonly mcpExclusions: readonly McpExclusion[] = [],\n ) {}\n\n // webpieces-disable no-function-outside-class -- static factory of this class\n static empty(): ApiDocRulesFindings {\n return new ApiDocRulesFindings(\n new ApiRuleFindings([], []),\n new ApiRuleFindings([], []),\n [],\n );\n }\n}\n\n/** The mode value that narrows the scan to the projects the diff touched. */\nexport const AFFECTED_PROJECT_MODE = 'AFFECTED_PROJECT';\n\n/**\n * ONE rule's switches, resolved from webpieces.config.json once per scan.\n *\n * There is NO default (#1017). Both rules are schema'd, so `webpieces.config.json` must carry an\n * entry for each or the config FAILS TO LOAD naming them — a consumer states `OFF`,\n * `AFFECTED_PROJECT` or `RUN_EVERY_TIME` out loud. The `off()` a bare constructor or a unit test\n * gets is not a default; it is what a caller that configured nothing asked for.\n */\nexport class ApiDocRule {\n constructor(\n public readonly name: string,\n public readonly enabled: boolean,\n /** Project roots this rule does not apply to — `allowedPaths` in the config. */\n public readonly allowedPaths: readonly string[],\n /**\n * Workspace-relative paths the diff touched, under `AFFECTED_PROJECT`; `null` under\n * `RUN_EVERY_TIME`, which means every project is in scope.\n *\n * `null` is also what an UNCOMPUTABLE diff resolves to — no merge-base, a shallow clone, no\n * repository at all. A diff that could not be read is not evidence that nothing changed, and\n * the only safe reading of \"I do not know what changed\" is \"look at all of it\".\n */\n public readonly changedPaths: readonly string[] | null = null,\n ) {}\n\n /** ARMED over every project — what a unit test constructs, and what RUN_EVERY_TIME resolves to. */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static armed(name: string): ApiDocRule {\n return new ApiDocRule(name, true, [], null);\n }\n\n /** ARMED over the projects owning one of `changedPaths` — what AFFECTED_PROJECT resolves to. */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static affected(name: string, changedPaths: readonly string[]): ApiDocRule {\n return new ApiDocRule(name, true, [], changedPaths);\n }\n\n // webpieces-disable no-function-outside-class -- static factory of this class\n static off(name: string): ApiDocRule {\n return new ApiDocRule(name, false, [], null);\n }\n\n /**\n * True when this rule scans the contracts of the project rooted at `root`.\n *\n * ONE place answers it, for both rules, so `allowedPaths` and the affected-project narrowing can\n * never be applied by one caller and skipped by another.\n */\n coversProject(root: string): boolean {\n if (!this.enabled) return false;\n if (matchesAnyGlob(root, this.allowedPaths)) return false;\n if (this.changedPaths === null) return true;\n const prefix = `${root}/`;\n return this.changedPaths.some((each: string): boolean => each.startsWith(prefix));\n }\n\n /** `mode: OFF` and the time-box/branch hatches come from RuleGate, so there is ONE reading. */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static fromConfig(workspaceRoot: string, name: string): ApiDocRule {\n if (new RuleGate().isDisabled(workspaceRoot, name, true)) {\n return ApiDocRule.off(name);\n }\n const rule = loadAndValidate(workspaceRoot).resolved.rules.get(name);\n const allowed = rule?.options['allowedPaths'];\n const allowedPaths = Array.isArray(allowed) ? (allowed as string[]) : [];\n const affected =\n rule?.options['mode'] === AFFECTED_PROJECT_MODE\n ? ApiDocRule.changedPathsOf(workspaceRoot)\n : null;\n return new ApiDocRule(name, true, allowedPaths, affected);\n }\n\n /**\n * Every path the diff touched, against the base nx itself uses (`NX_BASE`, else the merge-base\n * with origin/main). NOT ts-only and INCLUDING deletions: a deleted DTO changes what a project's\n * contracts can express exactly as an added one does, and a `project.json` edit can change which\n * project owns a contract at all.\n */\n // webpieces-disable no-function-outside-class -- private static reader of this class\n private static changedPathsOf(workspaceRoot: string): readonly string[] | null {\n const scope = new DiffScope();\n const range = scope.resolveBase(workspaceRoot);\n if (range.base === undefined) return null;\n const opts = new ChangedFilesOptions();\n opts.tsOnly = false;\n opts.includeDeletions = true;\n return scope.getChangedFiles(workspaceRoot, range.base, range.head, opts);\n }\n}\n\n/** ONE `webpieces-disable` naming a rule, and whether it gave a reason. */\nexport class DisableComment {\n constructor(public readonly hasReason: boolean) {}\n\n /**\n * The disable naming `rule` on the declaration at `line` (1-based) of `file`, or undefined.\n *\n * It walks UPWARD over the declaration's own leading trivia — blank lines, `//` comments, a\n * JSDoc block, and the decorators between them — because that is where an author writes one, and\n * it stops at the first line that is none of those, so a directive belonging to the PREVIOUS\n * declaration can never be read as covering this one.\n *\n * Only a directive NAMING this rule counts. An existing `// webpieces-disable no-any-unknown`\n * therefore does not silence these rules, which is the point: that rule answers \"is this\n * type-safe?\" and these answer \"is this field PUBLISHED with no shape?\" — different questions\n * with different right answers, so the second one wants its own, separately argued line.\n *\n * A file that cannot be read yields \"no disable\": a defect is still a defect, and inventing a\n * suppression out of an I/O failure is the one wrong answer.\n */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static readAt(file: string, line: number, rule: string): DisableComment | undefined {\n const lines = DisableComment.linesOf(file);\n if (lines === undefined) return undefined;\n const start = Math.max(0, line - 1);\n const onDeclaration = DisableComment.match(lines[start] ?? '', rule);\n if (onDeclaration !== undefined) return onDeclaration;\n for (let i = start - 1; i >= 0 && i >= start - DISABLE_LOOKBACK_LINES; i--) {\n const text = (lines[i] ?? '').trim();\n const above = DisableComment.match(text, rule);\n if (above !== undefined) return above;\n if (!DisableComment.isTrivia(text)) return undefined;\n }\n return undefined;\n }\n\n /** Leading trivia a disable comment is allowed to sit above: blanks, comments and decorators. */\n // webpieces-disable no-function-outside-class -- private static predicate of this class\n private static isTrivia(text: string): boolean {\n return (\n text === '' ||\n text.startsWith('//') ||\n text.startsWith('*') ||\n text.startsWith('/*') ||\n text.startsWith('@')\n );\n }\n\n // webpieces-disable no-function-outside-class -- private static reader of this class\n private static match(text: string, rule: string): DisableComment | undefined {\n const found = text.trim().match(DISABLE_RE);\n if (found === null) return undefined;\n const named = found[1].split(',').map((each: string): string => each.trim());\n if (!named.includes(rule)) return undefined;\n return new DisableComment((found[2] ?? '').trim() !== '');\n }\n\n /** The file's lines, or undefined when it cannot be read. */\n // webpieces-disable no-function-outside-class -- private static reader of this class\n private static linesOf(file: string): string[] | undefined {\n if (!fs.existsSync(file)) return undefined;\n return fs.readFileSync(file, 'utf8').split('\\n');\n }\n}\n"]}
|
|
@@ -79,7 +79,7 @@ export interface ApiScanResult {
|
|
|
79
79
|
rootUnions: RootUnionFindings;
|
|
80
80
|
/**
|
|
81
81
|
* `api-rules-for-openapi` / `api-rules-for-mcp` findings (#1011). Both ship OFF, so this is
|
|
82
|
-
* EMPTY unless a repo
|
|
82
|
+
* EMPTY unless a repo stated otherwise — no rule in this framework has a default (#1017).
|
|
83
83
|
*/
|
|
84
84
|
apiDocRules: ApiDocRulesFindings;
|
|
85
85
|
}
|
|
@@ -91,9 +91,9 @@ export declare class ApiUsageScanner {
|
|
|
91
91
|
private readonly externalApiPaths;
|
|
92
92
|
/** `no-root-union-api-type`'s switches — ARMED unless scanAndAttachApiRelations read otherwise. */
|
|
93
93
|
private readonly rootUnionRule;
|
|
94
|
-
/** `api-rules-for-openapi`'s switches —
|
|
94
|
+
/** `api-rules-for-openapi`'s switches — read from the config, which MUST state them (#1017). */
|
|
95
95
|
private readonly openApiRule;
|
|
96
|
-
/** `api-rules-for-mcp`'s switches —
|
|
96
|
+
/** `api-rules-for-mcp`'s switches — read from the config, which MUST state them (#1017). */
|
|
97
97
|
private readonly mcpRule;
|
|
98
98
|
private readonly locator;
|
|
99
99
|
private readonly relationsByProject;
|
|
@@ -106,9 +106,9 @@ export declare class ApiUsageScanner {
|
|
|
106
106
|
externalApiPaths?: readonly string[],
|
|
107
107
|
/** `no-root-union-api-type`'s switches — ARMED unless scanAndAttachApiRelations read otherwise. */
|
|
108
108
|
rootUnionRule?: RootUnionRule,
|
|
109
|
-
/** `api-rules-for-openapi`'s switches —
|
|
109
|
+
/** `api-rules-for-openapi`'s switches — read from the config, which MUST state them (#1017). */
|
|
110
110
|
openApiRule?: ApiDocRule,
|
|
111
|
-
/** `api-rules-for-mcp`'s switches —
|
|
111
|
+
/** `api-rules-for-mcp`'s switches — read from the config, which MUST state them (#1017). */
|
|
112
112
|
mcpRule?: ApiDocRule);
|
|
113
113
|
scan(): ApiScanResult;
|
|
114
114
|
private scanProject;
|
|
@@ -221,9 +221,9 @@ class ApiUsageScanner {
|
|
|
221
221
|
externalApiPaths = [],
|
|
222
222
|
/** `no-root-union-api-type`'s switches — ARMED unless scanAndAttachApiRelations read otherwise. */
|
|
223
223
|
rootUnionRule = root_union_scan_1.RootUnionRule.enabledEverywhere(),
|
|
224
|
-
/** `api-rules-for-openapi`'s switches —
|
|
224
|
+
/** `api-rules-for-openapi`'s switches — read from the config, which MUST state them (#1017). */
|
|
225
225
|
openApiRule = api_doc_rules_1.ApiDocRule.off(api_doc_rules_1.OPENAPI_RULE),
|
|
226
|
-
/** `api-rules-for-mcp`'s switches —
|
|
226
|
+
/** `api-rules-for-mcp`'s switches — read from the config, which MUST state them (#1017). */
|
|
227
227
|
mcpRule = api_doc_rules_1.ApiDocRule.off(api_doc_rules_1.MCP_RULE)) {
|
|
228
228
|
this.workspaceRoot = workspaceRoot;
|
|
229
229
|
this.projectInfos = projectInfos;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-scanner.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-scanner.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * API Usage Scanner\n *\n * Derives, by scanning real source (not a declaration file), how every project\n * relates to the api-lib projects it depends on. This is the single source of\n * truth for the `apiRelations` field in architecture/dependencies.json AND for\n * the runtime microservice graph.\n *\n * Signals (all resolved through the TypeScript checker, so re-exports resolve):\n * - IMPLEMENTS: `apiFactory.addRoutes(XxxApi, XxxController)` — the registration\n * that actually SERVES the contract over the wire. We deliberately\n * do NOT use `class Ctrl extends XxxApi`: a class can extend an API\n * as an in-process test double / simulator (e.g. Server2Simulator)\n * without ever serving it — only `addRoutes` proves a served route.\n * - USES: `factory.createRpcClient(XxxApi, ...)` → rpc client\n * `factory.createPubSubClient(XxxApi, ...)` → pubsub (Cloud Tasks) client\n * The config argument (`new ClientConfig('helper-fsdb')`) names WHICH service the\n * client talks to and is kept as `ApiRef.targetService` — see targetServiceOf.\n * An api-lib is DETECTED, not tagged: a project exporting an `abstract class`\n * carrying `@ApiPath` owns that API. Its transport is `@PubSub` → 'pubsub', else 'rpc'.\n *\n * Contracts are indexed from SOURCE in a pre-pass (ApiSourceIndexBuilder) rather than\n * from wherever the checker resolves an import to. A consumer without a tsconfig.base\n * `paths` entry resolves `import { XxxApi } from '@scope/xxx-api'` through node_modules\n * to the package's BUILT `dist/**.d.ts` — and tsc ERASES decorators when emitting\n * declarations, so `@ApiPath` can never be read there. Keying off the resolved\n * declaration therefore dropped whole services from the graph, silently. See\n * `recoverFromDeclaration`.\n */\n\nimport * as ts from 'typescript';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { matchesAnyGlob } from '@webpieces/rules-config';\nimport type { EnhancedGraph } from '../graph-sorter';\nimport { ProjectInfo } from '../project-info';\nimport { findProjectTsconfig } from '../di-graph/program';\nimport { resolveClassDeclaration } from '../di-graph/bindings';\nimport {\n ApiClassInfo,\n ApiContract,\n ApiContracts,\n ApiRef,\n ApiRelation,\n EmptiedApiContract,\n EndpointKind,\n NonLiteralDecoratorArg,\n ProjectApiRelations,\n UndeclaredExternalCaller,\n UndeclaredEndpointOperation,\n UnresolvedApiCall,\n UnresolvedEndpointPath,\n apiRefKey,\n deriveApiRelationKind,\n sortApiRefs,\n} from './api-relations';\nimport {\n ApiRulesForMcpError,\n ApiRulesForOpenApiError,\n EmptiedApiContractError,\n MissingBasePathError,\n RootUnionApiTypeError,\n UndeclaredExternalCallerError,\n UndeclaredEndpointOperationError,\n UnresolvedEndpointPathError,\n} from './api-contract-errors';\nimport { RootUnionFindings, RootUnionRule, RootUnionScan } from './root-union-scan';\nimport { ApiDocRule, ApiDocRulesFindings, MCP_RULE, OPENAPI_RULE } from './api-doc-rules';\nimport { ApiDocRulesScan } from './api-doc-rules-scan';\nimport {\n DecoratorArgDiagnostics,\n apiClassInfoFrom,\n apiClassInfoFromNode,\n calleeMethodName,\n collectTsFiles,\n constructorParamsOf,\n externalApiInfoFrom,\n implementedTypeNames,\n isAbstractClass,\n isTestFile,\n targetServiceOf,\n typeReferenceName,\n} from './api-ast';\n\nconst RPC_CLIENT_METHOD = 'createRpcClient';\nconst PUBSUB_CLIENT_METHOD = 'createPubSubClient';\nconst ADD_ROUTES_METHOD = 'addRoutes';\n\n/** The whole-workspace result of a scan. */\nexport interface ApiScanResult {\n /** projectName -> { apiLibProject -> relation }; only projects with ≥1 relation appear. */\n relationsByProject: Map<string, ProjectApiRelations>;\n /** Every project that owns ≥1 API contract class. */\n apiLibProjects: Set<string>;\n /** apiClassName -> where it lives + its transport. */\n apiIndex: Map<string, ApiClassInfo>;\n /**\n * Projects whose production (non-test) source was actually scanned. A project with only test\n * files (e.g. an e2e harness), or one the compiler couldn't load, is ABSENT — callers must not\n * conclude \"no implements/uses\" for it, because its behavior was never observed.\n */\n scannedProjects: Set<string>;\n /**\n * Call sites naming a contract we could not map back to workspace source. Non-empty means the\n * graph is INCOMPLETE — callers must surface these rather than emit a green, wrong graph.\n */\n unresolvedApiCalls: UnresolvedApiCall[];\n /**\n * Decorator arguments that were present but could not be reduced to a string (a cross-module\n * constant, a computed expression). Each one costs the graph a basePath, a method, or — when it\n * takes out every method of a class — the whole contract, so they must be surfaced.\n */\n nonLiteralDecoratorArgs: NonLiteralDecoratorArg[];\n /**\n * The subset of the above that is FATAL: an `@Endpoint` path that could not be read. Every client\n * builds its URL as `basePath + path`, so this is missing routing, not missing metadata —\n * buildApiContracts throws on a non-empty list rather than shipping a contract without it.\n */\n unresolvedEndpointPaths: UnresolvedEndpointPath[];\n /**\n * Contract classes that declared `@Endpoint` methods and kept none — the exact shape that used to\n * slip out through buildApiContracts' zero-method skip, taking a whole service's queues with it.\n */\n emptiedApiContracts: EmptiedApiContract[];\n /**\n * `external` endpoints that did not say WHO calls them. Fatal: the inbound box on the runtime\n * graph exists to name that system, and with nothing to name it restates our own contract name.\n */\n undeclaredExternalCallers: UndeclaredExternalCaller[];\n /** Endpoints lacking the explicit side-effect contract used for retry safety and MCP hints. */\n undeclaredEndpointOperations: UndeclaredEndpointOperation[];\n /** `no-root-union-api-type`'s findings. Fatal in buildApiContracts — see root-union-scan.ts. */\n rootUnions: RootUnionFindings;\n /**\n * `api-rules-for-openapi` / `api-rules-for-mcp` findings (#1011). Both ship OFF, so this is\n * EMPTY unless a repo opted in — see `defaultRules` in `@webpieces/rules-config` for why.\n */\n apiDocRules: ApiDocRulesFindings;\n}\n\n/** Maps an absolute source-file path to the workspace project that owns it (longest-root-prefix). */\nclass ProjectLocator {\n private readonly roots: ProjectRoot[];\n\n constructor(workspaceRoot: string, projectInfos: Map<string, ProjectInfo>) {\n const roots: ProjectRoot[] = [];\n for (const info of projectInfos.values()) {\n if (info.root === '' || info.root === '.') continue;\n roots.push(new ProjectRoot(info.name, path.resolve(workspaceRoot, info.root)));\n }\n // Longest root first so a nested project wins over its parent.\n this.roots = roots.sort((a: ProjectRoot, b: ProjectRoot) => b.abs.length - a.abs.length);\n }\n\n projectOf(absFile: string): string | null {\n const normalized = path.resolve(absFile);\n for (const root of this.roots) {\n if (normalized === root.abs || normalized.startsWith(root.abs + path.sep))\n return root.name;\n }\n return null;\n }\n}\n\nclass ProjectRoot {\n constructor(\n public readonly name: string,\n public readonly abs: string,\n ) {}\n}\n\n/**\n * Every API contract in the workspace, keyed by class name, read from SOURCE.\n *\n * Name-keyed because a call site only ever gives us a name once its import has resolved into a\n * decorator-erased declaration. Two api-libs exporting the same class name collide (last wins) —\n * the same collision the published `apiIndex` has always had.\n */\nclass ApiSourceIndex {\n constructor(\n public readonly byName: Map<string, ApiClassInfo>,\n public readonly owners: Set<string>,\n ) {}\n\n lookup(api: string): ApiClassInfo | null {\n return this.byName.get(api) ?? null;\n }\n}\n\n/**\n * Builds the ApiSourceIndex by parsing each project's own `src/**` directly.\n *\n * Deliberately parser-only (no ts.Program, no checker): we need the decorators exactly as\n * written, and a plain parse cannot be diverted to a `.d.ts` by module resolution — which is\n * the entire bug this guards against. It is also cheap enough to run over every project.\n */\nclass ApiSourceIndexBuilder {\n private readonly byName = new Map<string, ApiClassInfo>();\n private readonly owners = new Set<string>();\n\n constructor(\n private readonly workspaceRoot: string,\n private readonly projectInfos: Map<string, ProjectInfo>,\n /** Globs of project roots holding vendor contracts — see ExternalApiIndex. */\n private readonly externalApiPaths: readonly string[],\n /** Sink for decorator arguments this parser-only pass cannot reduce to a string. */\n private readonly diagnostics: DecoratorArgDiagnostics,\n ) {}\n\n build(): ApiSourceIndex {\n for (const info of this.projectInfos.values()) {\n if (info.root === '' || info.root === '.') continue;\n this.indexProject(info);\n }\n return new ApiSourceIndex(this.byName, this.owners);\n }\n\n private indexProject(info: ProjectInfo): void {\n const srcDir = path.join(path.resolve(this.workspaceRoot, info.root), 'src');\n if (!fs.existsSync(srcDir)) return;\n const external = matchesAnyGlob(info.root, this.externalApiPaths);\n for (const file of collectTsFiles(srcDir)) {\n if (isTestFile(file)) continue; // tests are not production topology\n const text = fs.readFileSync(file, 'utf8');\n const sourceFile = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);\n this.indexNode(sourceFile, info.name, external);\n }\n }\n\n private indexNode(node: ts.Node, project: string, external: boolean): void {\n const info = external\n ? externalApiInfoFrom(node, project)\n : apiClassInfoFromNode(node, project, this.diagnostics);\n if (info) {\n this.owners.add(project);\n this.byName.set(info.api, info);\n }\n ts.forEachChild(node, (child: ts.Node) => this.indexNode(child, project, external));\n }\n}\n/** Per-owner accumulator that dedupes API refs while a single project is scanned. */\nclass RelationAccumulator {\n private readonly implementsByOwner = new Map<string, Map<string, ApiRef>>();\n private readonly usesByOwner = new Map<string, Map<string, ApiRef>>();\n\n addImplements(owner: string, ref: ApiRef): void {\n ensureRefMap(this.implementsByOwner, owner).set(apiRefKey(ref), ref);\n }\n\n /**\n * Keyed by api + targetService: one project legitimately binds the SAME contract against two\n * different services (a WarmupApi client per data server), and those are two relations, not one.\n */\n addUses(owner: string, ref: ApiRef): void {\n ensureRefMap(this.usesByOwner, owner).set(apiRefKey(ref), ref);\n }\n\n /** Build the deterministic { owner -> relation } record, owners in sorted order. */\n toRelations(): ProjectApiRelations {\n const owners = new Set<string>([\n ...this.implementsByOwner.keys(),\n ...this.usesByOwner.keys(),\n ]);\n const relations: ProjectApiRelations = {};\n for (const owner of [...owners].sort()) {\n const implementsRefs = sortApiRefs([\n ...(this.implementsByOwner.get(owner)?.values() ?? []),\n ]);\n const usesRefs = sortApiRefs([...(this.usesByOwner.get(owner)?.values() ?? [])]);\n const relation: ApiRelation = {\n kind: deriveApiRelationKind(implementsRefs, usesRefs),\n implements: implementsRefs,\n uses: usesRefs,\n };\n relations[owner] = relation;\n }\n return relations;\n }\n\n isEmpty(): boolean {\n return this.implementsByOwner.size === 0 && this.usesByOwner.size === 0;\n }\n}\n\n// webpieces-disable no-function-outside-class -- tiny map helper, matching the AST-helper style of di-graph/bindings.ts\nfunction ensureRefMap(map: Map<string, Map<string, ApiRef>>, owner: string): Map<string, ApiRef> {\n let inner = map.get(owner);\n if (!inner) {\n inner = new Map<string, ApiRef>();\n map.set(owner, inner);\n }\n return inner;\n}\n\n/** Statically scans every project for its api-lib implements/uses relationships. */\nexport class ApiUsageScanner {\n private readonly locator: ProjectLocator;\n private readonly relationsByProject = new Map<string, ProjectApiRelations>();\n private readonly scannedProjects = new Set<string>();\n private readonly unresolvedApiCalls: UnresolvedApiCall[] = [];\n private readonly decoratorArgDiagnostics: DecoratorArgDiagnostics;\n private sourceIndex = new ApiSourceIndex(new Map<string, ApiClassInfo>(), new Set<string>());\n\n constructor(\n private readonly workspaceRoot: string,\n private readonly projectInfos: Map<string, ProjectInfo>,\n /** Globs of project roots whose exported `*Api` types are contracts for outside systems. */\n private readonly externalApiPaths: readonly string[] = [],\n /** `no-root-union-api-type`'s switches — ARMED unless scanAndAttachApiRelations read otherwise. */\n private readonly rootUnionRule: RootUnionRule = RootUnionRule.enabledEverywhere(),\n /** `api-rules-for-openapi`'s switches — OFF unless a repo opted in. */\n private readonly openApiRule: ApiDocRule = ApiDocRule.off(OPENAPI_RULE),\n /** `api-rules-for-mcp`'s switches — OFF unless a repo opted in. */\n private readonly mcpRule: ApiDocRule = ApiDocRule.off(MCP_RULE),\n ) {\n this.locator = new ProjectLocator(workspaceRoot, projectInfos);\n this.decoratorArgDiagnostics = new DecoratorArgDiagnostics(workspaceRoot);\n }\n\n scan(): ApiScanResult {\n // Pre-pass: every contract, from source, BEFORE any call site is resolved — a call site in\n // one project routinely names a contract owned by a project we have not walked yet.\n this.sourceIndex = new ApiSourceIndexBuilder(\n this.workspaceRoot,\n this.projectInfos,\n this.externalApiPaths,\n this.decoratorArgDiagnostics,\n ).build();\n for (const info of this.projectInfos.values()) {\n if (info.root === '' || info.root === '.') continue;\n this.scanProject(info);\n }\n return {\n relationsByProject: this.relationsByProject,\n apiLibProjects: this.sourceIndex.owners,\n apiIndex: this.sourceIndex.byName,\n scannedProjects: this.scannedProjects,\n unresolvedApiCalls: this.unresolvedApiCalls,\n nonLiteralDecoratorArgs: this.decoratorArgDiagnostics.all(),\n unresolvedEndpointPaths: this.decoratorArgDiagnostics.unresolvedEndpointPaths(),\n emptiedApiContracts: this.decoratorArgDiagnostics.emptiedContracts(),\n undeclaredExternalCallers: this.decoratorArgDiagnostics.undeclaredExternalCallers(),\n undeclaredEndpointOperations:\n this.decoratorArgDiagnostics.undeclaredEndpointOperations(),\n rootUnions: new RootUnionScan(\n this.workspaceRoot,\n this.projectInfos,\n this.rootUnionRule,\n ).run(),\n apiDocRules: new ApiDocRulesScan(\n this.workspaceRoot,\n this.projectInfos,\n this.openApiRule,\n this.mcpRule,\n ).run(),\n };\n }\n\n private scanProject(info: ProjectInfo): void {\n const program = createScanProgram(path.resolve(this.workspaceRoot, info.root));\n if (!program) return;\n const checker = program.getTypeChecker();\n const accumulator = new RelationAccumulator();\n let scannedProductionFile = false;\n\n for (const sourceFile of program.getSourceFiles()) {\n if (sourceFile.isDeclarationFile || sourceFile.fileName.includes('/node_modules/'))\n continue;\n if (isTestFile(sourceFile.fileName)) continue; // tests are not production topology\n // Only this project's OWN files — imported api-lib source is in the program too.\n if (this.locator.projectOf(sourceFile.fileName) !== info.name) continue;\n scannedProductionFile = true;\n this.visit(sourceFile, checker, info.name, accumulator);\n }\n\n // Record coverage only when we actually saw production source — an all-test project (e2e)\n // stays absent so the validator won't wrongly flag its api-lib deps as unused.\n if (scannedProductionFile) this.scannedProjects.add(info.name);\n if (!accumulator.isEmpty())\n this.relationsByProject.set(info.name, accumulator.toRelations());\n }\n\n private visit(\n node: ts.Node,\n checker: ts.TypeChecker,\n project: string,\n acc: RelationAccumulator,\n ): void {\n // In-repo contract classes are indexed by the source pre-pass, so only calls matter for them.\n if (ts.isCallExpression(node)) this.recordCall(node, checker, project, acc);\n // A VENDOR contract has no client-factory call site to key off — it arrives by injection —\n // so classes have to be inspected too.\n if (ts.isClassDeclaration(node)) this.recordExternalUses(node, acc);\n ts.forEachChild(node, (child: ts.Node) => this.visit(child, checker, project, acc));\n }\n\n /**\n * Record a `uses` for every vendor contract this class receives by CONSTRUCTOR INJECTION —\n * `constructor(@inject(GMAIL_TYPES.GmailApi) private readonly gmail: GmailApi)`.\n *\n * The parameter TYPE is the signal, not the token: a token is an opaque Symbol whose name we\n * would have to guess at, while the type is written right there and is what the class actually\n * calls. Matching happens by name against the external index, so an import that resolves to a\n * built `.d.ts` works exactly as well as one resolving to source.\n *\n * A class that IMPLEMENTS the contract is skipped — that is the vendor adapter (`GmailClient`)\n * or a test double (`InMemoryFirestore`, `MockTts`), which IS the seam rather than a caller of\n * it. Counting those would draw an edge from every service embedding a fake to a vendor it never\n * actually reaches.\n */\n private recordExternalUses(cls: ts.ClassDeclaration, acc: RelationAccumulator): void {\n const implemented = implementedTypeNames(cls);\n for (const param of constructorParamsOf(cls)) {\n const typeName = typeReferenceName(param.type);\n if (typeName === null || implemented.has(typeName)) continue;\n const info = this.sourceIndex.lookup(typeName);\n if (info === null || info.type !== 'external') continue;\n acc.addUses(info.owner, { api: info.api, type: 'external' });\n }\n }\n\n private recordCall(\n call: ts.CallExpression,\n checker: ts.TypeChecker,\n project: string,\n acc: RelationAccumulator,\n ): void {\n const method = calleeMethodName(call);\n if (method === null || call.arguments.length === 0) return;\n if (method === ADD_ROUTES_METHOD) {\n const info = this.apiInfoFromExpr(call.arguments[0], checker, project);\n if (info) acc.addImplements(info.owner, { api: info.api, type: info.type });\n return;\n }\n if (method === RPC_CLIENT_METHOD || method === PUBSUB_CLIENT_METHOD) {\n const info = this.apiInfoFromExpr(call.arguments[0], checker, project);\n if (!info) return;\n // Argument 2 names WHICH service this client talks to. Keeping it is what lets the\n // runtime graph draw ONE edge instead of one per implementer of the contract.\n const targetService = targetServiceOf(call);\n const ref: ApiRef = { api: info.api, type: info.type };\n if (targetService !== null) ref.targetService = targetService;\n acc.addUses(info.owner, ref);\n }\n }\n\n /** Resolve an expression to the API contract it names, or null if it is not one. */\n private apiInfoFromExpr(\n expr: ts.Expression,\n checker: ts.TypeChecker,\n project: string,\n ): ApiClassInfo | null {\n const decl = resolveClassDeclaration(expr, checker);\n if (!decl) return null;\n const fromSource = this.apiClassInfoFor(decl);\n return fromSource ?? this.recoverFromDeclaration(decl, expr, project);\n }\n\n /**\n * The checker landed on a BUILT declaration instead of source — the consumer has no\n * tsconfig.base `paths` entry for the api-lib, so the import went through node_modules to\n * `dist/**.d.ts`. tsc erases decorators when emitting declarations, so `@ApiPath` is simply\n * not there and never will be. Recover the contract by name from the source index; the graph\n * is then correct no matter how the consumer's tsconfig is laid out.\n */\n private recoverFromDeclaration(\n decl: ts.ClassDeclaration,\n expr: ts.Expression,\n project: string,\n ): ApiClassInfo | null {\n // An abstract class is the shape of a contract; a non-abstract argument is genuinely not one.\n if (!decl.getSourceFile().isDeclarationFile || !isAbstractClass(decl) || !decl.name)\n return null;\n const recovered = this.sourceIndex.lookup(decl.name.text);\n if (recovered) return recovered;\n // Abstract, in a .d.ts, yet no workspace source owns it — the scan is blind here. Say so.\n this.unresolvedApiCalls.push(\n new UnresolvedApiCall(\n project,\n decl.name.text,\n this.relativeLocation(expr),\n this.relativePath(decl.getSourceFile().fileName),\n ),\n );\n return null;\n }\n\n /** `path/to/file.ts:LINE` for `node`, workspace-relative, for a human-readable report. */\n private relativeLocation(node: ts.Node): string {\n const sourceFile = node.getSourceFile();\n const position = sourceFile.getLineAndCharacterOfPosition(node.getStart());\n return `${this.relativePath(sourceFile.fileName)}:${position.line + 1}`;\n }\n\n private relativePath(absFile: string): string {\n return path.relative(this.workspaceRoot, absFile);\n }\n\n /**\n * {api, owner, type, methods} when `cls` is an `abstract class` carrying `@ApiPath` IN SOURCE,\n * else null. Only the OWNER differs from the index pre-pass — here it comes from the file's\n * location rather than from the project being walked — so the contract test itself is delegated\n * to apiClassInfoFrom, keeping one definition of \"this is a contract\".\n */\n private apiClassInfoFor(cls: ts.ClassDeclaration): ApiClassInfo | null {\n const owner = this.locator.projectOf(cls.getSourceFile().fileName);\n if (owner === null) return null;\n return apiClassInfoFrom(cls, owner);\n }\n}\n\n/**\n * Run the scan and attach the derived `apiRelations` onto each graph entry in\n * place. Shared by `architecture:generate` (which then saves) and\n * `architecture:validate-architecture-unchanged` (which regenerates in memory\n * and must attach the SAME field, or it would see a phantom diff). Returns the\n * full scan so callers (validators, runtime graph) can reuse the api index.\n */\n// webpieces-disable no-function-outside-class -- module entry point, mirrors generateReducedGraph/collectBindings\nexport function scanAndAttachApiRelations(\n workspaceRoot: string,\n graph: EnhancedGraph,\n projectInfos: Map<string, ProjectInfo>,\n externalApiPaths: readonly string[] = [],\n): ApiScanResult {\n const result = new ApiUsageScanner(\n workspaceRoot,\n projectInfos,\n externalApiPaths,\n RootUnionRule.fromConfig(workspaceRoot),\n ApiDocRule.fromConfig(workspaceRoot, OPENAPI_RULE),\n ApiDocRule.fromConfig(workspaceRoot, MCP_RULE),\n ).scan();\n for (const projectName of result.relationsByProject.keys()) {\n const entry = graph[projectName];\n if (entry) entry.apiRelations = result.relationsByProject.get(projectName);\n }\n return result;\n}\n\n/**\n * The committed api contract table (one `architecture/apis/<ApiName>.json` per entry), from a\n * completed scan.\n *\n * Only contracts with ≥1 endpoint are emitted: a vendor seam has no routes, so a table entry for it\n * would be an empty shell, and its identity is already carried by the `external` refs in\n * apiRelations. Sorted by api name, methods left in declaration order, so the file is deterministic.\n *\n * THROWS on the ways an entry can be wrong-but-green, checked root cause first:\n * 1. an `@Endpoint` path the scan could not read (UnresolvedEndpointPathError) — the other half of\n * the URL a consumer computes, and the cause of most emptied contracts;\n * 2. a class that declared endpoints and kept none (EmptiedApiContractError), which would otherwise\n * leave silently through the zero-method skip above;\n * 3. a method without an explicit operation (UndeclaredEndpointOperationError);\n * 4. an `external` method that never said WHO calls it (UndeclaredExternalCallerError);\n * 5. a routed contract with no basePath (MissingBasePathError).\n * All are worse than an absent entry: a consumer joining `basePath + path` computes a\n * confidently wrong URL with no signal that anything is off, because every other entry is complete.\n * Each error aggregates EVERY offender, so a developer fixing five constants sees five in one run.\n */\n// webpieces-disable no-function-outside-class -- module entry point, mirrors scanAndAttachApiRelations\nexport function buildApiContracts(scan: ApiScanResult): ApiContracts {\n // Root cause before symptom: an unreadable path is what empties a contract, so naming the paths\n // is what the author can actually act on.\n if (scan.unresolvedEndpointPaths.length > 0)\n throw new UnresolvedEndpointPathError(scan.unresolvedEndpointPaths);\n if (scan.emptiedApiContracts.length > 0)\n throw new EmptiedApiContractError(scan.emptiedApiContracts);\n if (scan.undeclaredEndpointOperations.length > 0) {\n throw new UndeclaredEndpointOperationError(scan.undeclaredEndpointOperations);\n }\n // A shape no function-calling API will accept, read perfectly well — unlike the four above, which\n // are contracts the scan could not READ at all.\n if (!scan.rootUnions.isEmpty()) throw new RootUnionApiTypeError(scan.rootUnions);\n // Contract shapes that are not PUBLISHABLE, read by the generator's own extractor (#1011).\n // After the root union, which is the one shape no function-calling API will accept at all, and\n // before the caller checks below, which are about the architecture graph rather than a document.\n if (!scan.apiDocRules.openApi.isEmpty()) {\n throw new ApiRulesForOpenApiError(scan.apiDocRules.openApi);\n }\n if (!scan.apiDocRules.mcp.isEmpty()) {\n throw new ApiRulesForMcpError(scan.apiDocRules.mcp, scan.apiDocRules.mcpExclusions);\n }\n // After the two above: an unreadable path is what empties a contract, and a contract that lost\n // every method has no external endpoint left to complain about.\n if (scan.undeclaredExternalCallers.length > 0) {\n throw new UndeclaredExternalCallerError(scan.undeclaredExternalCallers);\n }\n const contracts: ApiContracts = {};\n const missing: string[] = [];\n for (const api of [...scan.apiIndex.keys()].sort()) {\n const info = scan.apiIndex.get(api)!;\n if (info.methods.length === 0) continue;\n if (info.basePath === undefined) {\n missing.push(`${api} (owner ${info.owner})`);\n continue;\n }\n const contract: ApiContract = {\n owner: info.owner,\n apiKind: info.type,\n basePath: info.basePath,\n methods: info.methods,\n };\n contracts[api] = contract;\n }\n if (missing.length > 0) throw new MissingBasePathError(missing);\n return contracts;\n}\n\n/**\n * Loud, actionable report for decorator arguments the scan could not reduce to a string.\n *\n * Same-module constants resolve, so anything reaching here is genuinely out of reach of a\n * parser-only pass — and every one of them silently shrinks the graph. Empty string when there is\n * nothing to say, so callers can test it without special-casing.\n */\n// webpieces-disable no-function-outside-class -- pure formatter, mirrors describeUnresolvedApiCalls\nexport function describeNonLiteralDecoratorArgs(args: readonly NonLiteralDecoratorArg[]): string {\n if (args.length === 0) return '';\n const lines = [\n `⚠️ ${args.length} decorator argument(s) are not string literals and could not be resolved.`,\n ` Each one drops data from the graph: a missing basePath, a missing method, or a whole contract:`,\n ];\n for (const arg of args) {\n const where = arg.method === null ? arg.api : `${arg.api}.${arg.method}`;\n lines.push(` • @${arg.decorator}(${arg.argument}) on ${where} at ${arg.at}`);\n }\n lines.push(\n ` A constant declared in the SAME module resolves. One imported from another module does not —`,\n ` this scan is parser-only by design (module resolution can land on a decorator-erased .d.ts).`,\n ` Fix by inlining the string literal, or by moving the constant into the contract's own module.`,\n );\n return lines.join('\\n');\n}\n\n/**\n * Every contract method whose declared @Endpoint kind its api kind cannot deliver — an rpc method on\n * a @PubSub contract (nothing calls a queue synchronously), or a cloudtasks/cron method on an @Rpc\n * contract (naming a queue or schedule nothing could deliver to). Mirrors core-util's\n * ENDPOINT_KINDS_BY_API_KIND at BUILD time, where it can name the file instead of throwing at wiring.\n */\n// webpieces-disable no-function-outside-class -- pure formatter, mirrors describeUnresolvedApiCalls\nexport function describeMismatchedEndpointKinds(contracts: ApiContracts): string[] {\n const allowedByKind: Record<string, readonly EndpointKind[]> = {\n rpc: ['rpc', 'external'],\n pubsub: ['cloudtasks', 'cron', 'external'],\n };\n const problems: string[] = [];\n for (const api of Object.keys(contracts)) {\n const contract = contracts[api];\n const allowed = allowedByKind[contract.apiKind];\n if (allowed === undefined) continue;\n for (const method of contract.methods) {\n if (allowed.includes(method.kind)) continue;\n problems.push(\n `${api}.${method.name} declares @Endpoint(${method.httpMethod ?? 'POST'}, '${method.path}', ${method.operation}, ${method.kind}) but ${api} is ` +\n `@${contract.apiKind === 'pubsub' ? 'PubSub' : 'Rpc'} — allowed kinds are ${allowed.join(' | ')}.`,\n );\n }\n }\n return problems;\n}\n\n/**\n * Build a program for scanning ONE project. Prefers the project's compile tsconfig; but when that\n * is a solution-style tsconfig (only `references`, no `files`/`include` — e.g. legacy-server), it\n * yields zero files, so we fall back to globbing the project's own `src/**` and reuse the resolved\n * compiler options (which carry tsconfig.base `paths` for cross-package @webpieces resolution).\n *\n * `paths` is a PREFERENCE, not a precondition: it lets imports resolve straight to source. Without\n * it they land on a decorator-erased `dist/**.d.ts`, which the source index recovers from — see\n * ApiUsageScanner.recoverFromDeclaration.\n */\n// webpieces-disable no-function-outside-class -- ts Program factory, mirrors di-graph/program.ts\nfunction createScanProgram(projectRootAbs: string): ts.Program | null {\n const configPath = findProjectTsconfig(projectRootAbs);\n if (!configPath) return buildProgramFromSrc(projectRootAbs, {});\n const host = Object.assign({}, ts.sys, {\n onUnRecoverableConfigFileDiagnostic: (): void => undefined,\n }) as ts.ParseConfigFileHost;\n const parsed = ts.getParsedCommandLineOfConfigFile(configPath, {}, host);\n if (!parsed) return null;\n if (parsed.fileNames.length > 0) return ts.createProgram(parsed.fileNames, parsed.options);\n return buildProgramFromSrc(projectRootAbs, parsed.options);\n}\n\n// webpieces-disable no-function-outside-class -- ts Program factory helper, mirrors di-graph/program.ts\nfunction buildProgramFromSrc(\n projectRootAbs: string,\n options: ts.CompilerOptions,\n): ts.Program | null {\n const srcDir = path.join(projectRootAbs, 'src');\n if (!fs.existsSync(srcDir)) return null;\n const files = collectTsFiles(srcDir);\n return files.length > 0 ? ts.createProgram(files, options) : null;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"api-scanner.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-scanner.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * API Usage Scanner\n *\n * Derives, by scanning real source (not a declaration file), how every project\n * relates to the api-lib projects it depends on. This is the single source of\n * truth for the `apiRelations` field in architecture/dependencies.json AND for\n * the runtime microservice graph.\n *\n * Signals (all resolved through the TypeScript checker, so re-exports resolve):\n * - IMPLEMENTS: `apiFactory.addRoutes(XxxApi, XxxController)` — the registration\n * that actually SERVES the contract over the wire. We deliberately\n * do NOT use `class Ctrl extends XxxApi`: a class can extend an API\n * as an in-process test double / simulator (e.g. Server2Simulator)\n * without ever serving it — only `addRoutes` proves a served route.\n * - USES: `factory.createRpcClient(XxxApi, ...)` → rpc client\n * `factory.createPubSubClient(XxxApi, ...)` → pubsub (Cloud Tasks) client\n * The config argument (`new ClientConfig('helper-fsdb')`) names WHICH service the\n * client talks to and is kept as `ApiRef.targetService` — see targetServiceOf.\n * An api-lib is DETECTED, not tagged: a project exporting an `abstract class`\n * carrying `@ApiPath` owns that API. Its transport is `@PubSub` → 'pubsub', else 'rpc'.\n *\n * Contracts are indexed from SOURCE in a pre-pass (ApiSourceIndexBuilder) rather than\n * from wherever the checker resolves an import to. A consumer without a tsconfig.base\n * `paths` entry resolves `import { XxxApi } from '@scope/xxx-api'` through node_modules\n * to the package's BUILT `dist/**.d.ts` — and tsc ERASES decorators when emitting\n * declarations, so `@ApiPath` can never be read there. Keying off the resolved\n * declaration therefore dropped whole services from the graph, silently. See\n * `recoverFromDeclaration`.\n */\n\nimport * as ts from 'typescript';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { matchesAnyGlob } from '@webpieces/rules-config';\nimport type { EnhancedGraph } from '../graph-sorter';\nimport { ProjectInfo } from '../project-info';\nimport { findProjectTsconfig } from '../di-graph/program';\nimport { resolveClassDeclaration } from '../di-graph/bindings';\nimport {\n ApiClassInfo,\n ApiContract,\n ApiContracts,\n ApiRef,\n ApiRelation,\n EmptiedApiContract,\n EndpointKind,\n NonLiteralDecoratorArg,\n ProjectApiRelations,\n UndeclaredExternalCaller,\n UndeclaredEndpointOperation,\n UnresolvedApiCall,\n UnresolvedEndpointPath,\n apiRefKey,\n deriveApiRelationKind,\n sortApiRefs,\n} from './api-relations';\nimport {\n ApiRulesForMcpError,\n ApiRulesForOpenApiError,\n EmptiedApiContractError,\n MissingBasePathError,\n RootUnionApiTypeError,\n UndeclaredExternalCallerError,\n UndeclaredEndpointOperationError,\n UnresolvedEndpointPathError,\n} from './api-contract-errors';\nimport { RootUnionFindings, RootUnionRule, RootUnionScan } from './root-union-scan';\nimport { ApiDocRule, ApiDocRulesFindings, MCP_RULE, OPENAPI_RULE } from './api-doc-rules';\nimport { ApiDocRulesScan } from './api-doc-rules-scan';\nimport {\n DecoratorArgDiagnostics,\n apiClassInfoFrom,\n apiClassInfoFromNode,\n calleeMethodName,\n collectTsFiles,\n constructorParamsOf,\n externalApiInfoFrom,\n implementedTypeNames,\n isAbstractClass,\n isTestFile,\n targetServiceOf,\n typeReferenceName,\n} from './api-ast';\n\nconst RPC_CLIENT_METHOD = 'createRpcClient';\nconst PUBSUB_CLIENT_METHOD = 'createPubSubClient';\nconst ADD_ROUTES_METHOD = 'addRoutes';\n\n/** The whole-workspace result of a scan. */\nexport interface ApiScanResult {\n /** projectName -> { apiLibProject -> relation }; only projects with ≥1 relation appear. */\n relationsByProject: Map<string, ProjectApiRelations>;\n /** Every project that owns ≥1 API contract class. */\n apiLibProjects: Set<string>;\n /** apiClassName -> where it lives + its transport. */\n apiIndex: Map<string, ApiClassInfo>;\n /**\n * Projects whose production (non-test) source was actually scanned. A project with only test\n * files (e.g. an e2e harness), or one the compiler couldn't load, is ABSENT — callers must not\n * conclude \"no implements/uses\" for it, because its behavior was never observed.\n */\n scannedProjects: Set<string>;\n /**\n * Call sites naming a contract we could not map back to workspace source. Non-empty means the\n * graph is INCOMPLETE — callers must surface these rather than emit a green, wrong graph.\n */\n unresolvedApiCalls: UnresolvedApiCall[];\n /**\n * Decorator arguments that were present but could not be reduced to a string (a cross-module\n * constant, a computed expression). Each one costs the graph a basePath, a method, or — when it\n * takes out every method of a class — the whole contract, so they must be surfaced.\n */\n nonLiteralDecoratorArgs: NonLiteralDecoratorArg[];\n /**\n * The subset of the above that is FATAL: an `@Endpoint` path that could not be read. Every client\n * builds its URL as `basePath + path`, so this is missing routing, not missing metadata —\n * buildApiContracts throws on a non-empty list rather than shipping a contract without it.\n */\n unresolvedEndpointPaths: UnresolvedEndpointPath[];\n /**\n * Contract classes that declared `@Endpoint` methods and kept none — the exact shape that used to\n * slip out through buildApiContracts' zero-method skip, taking a whole service's queues with it.\n */\n emptiedApiContracts: EmptiedApiContract[];\n /**\n * `external` endpoints that did not say WHO calls them. Fatal: the inbound box on the runtime\n * graph exists to name that system, and with nothing to name it restates our own contract name.\n */\n undeclaredExternalCallers: UndeclaredExternalCaller[];\n /** Endpoints lacking the explicit side-effect contract used for retry safety and MCP hints. */\n undeclaredEndpointOperations: UndeclaredEndpointOperation[];\n /** `no-root-union-api-type`'s findings. Fatal in buildApiContracts — see root-union-scan.ts. */\n rootUnions: RootUnionFindings;\n /**\n * `api-rules-for-openapi` / `api-rules-for-mcp` findings (#1011). Both ship OFF, so this is\n * EMPTY unless a repo stated otherwise — no rule in this framework has a default (#1017).\n */\n apiDocRules: ApiDocRulesFindings;\n}\n\n/** Maps an absolute source-file path to the workspace project that owns it (longest-root-prefix). */\nclass ProjectLocator {\n private readonly roots: ProjectRoot[];\n\n constructor(workspaceRoot: string, projectInfos: Map<string, ProjectInfo>) {\n const roots: ProjectRoot[] = [];\n for (const info of projectInfos.values()) {\n if (info.root === '' || info.root === '.') continue;\n roots.push(new ProjectRoot(info.name, path.resolve(workspaceRoot, info.root)));\n }\n // Longest root first so a nested project wins over its parent.\n this.roots = roots.sort((a: ProjectRoot, b: ProjectRoot) => b.abs.length - a.abs.length);\n }\n\n projectOf(absFile: string): string | null {\n const normalized = path.resolve(absFile);\n for (const root of this.roots) {\n if (normalized === root.abs || normalized.startsWith(root.abs + path.sep))\n return root.name;\n }\n return null;\n }\n}\n\nclass ProjectRoot {\n constructor(\n public readonly name: string,\n public readonly abs: string,\n ) {}\n}\n\n/**\n * Every API contract in the workspace, keyed by class name, read from SOURCE.\n *\n * Name-keyed because a call site only ever gives us a name once its import has resolved into a\n * decorator-erased declaration. Two api-libs exporting the same class name collide (last wins) —\n * the same collision the published `apiIndex` has always had.\n */\nclass ApiSourceIndex {\n constructor(\n public readonly byName: Map<string, ApiClassInfo>,\n public readonly owners: Set<string>,\n ) {}\n\n lookup(api: string): ApiClassInfo | null {\n return this.byName.get(api) ?? null;\n }\n}\n\n/**\n * Builds the ApiSourceIndex by parsing each project's own `src/**` directly.\n *\n * Deliberately parser-only (no ts.Program, no checker): we need the decorators exactly as\n * written, and a plain parse cannot be diverted to a `.d.ts` by module resolution — which is\n * the entire bug this guards against. It is also cheap enough to run over every project.\n */\nclass ApiSourceIndexBuilder {\n private readonly byName = new Map<string, ApiClassInfo>();\n private readonly owners = new Set<string>();\n\n constructor(\n private readonly workspaceRoot: string,\n private readonly projectInfos: Map<string, ProjectInfo>,\n /** Globs of project roots holding vendor contracts — see ExternalApiIndex. */\n private readonly externalApiPaths: readonly string[],\n /** Sink for decorator arguments this parser-only pass cannot reduce to a string. */\n private readonly diagnostics: DecoratorArgDiagnostics,\n ) {}\n\n build(): ApiSourceIndex {\n for (const info of this.projectInfos.values()) {\n if (info.root === '' || info.root === '.') continue;\n this.indexProject(info);\n }\n return new ApiSourceIndex(this.byName, this.owners);\n }\n\n private indexProject(info: ProjectInfo): void {\n const srcDir = path.join(path.resolve(this.workspaceRoot, info.root), 'src');\n if (!fs.existsSync(srcDir)) return;\n const external = matchesAnyGlob(info.root, this.externalApiPaths);\n for (const file of collectTsFiles(srcDir)) {\n if (isTestFile(file)) continue; // tests are not production topology\n const text = fs.readFileSync(file, 'utf8');\n const sourceFile = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);\n this.indexNode(sourceFile, info.name, external);\n }\n }\n\n private indexNode(node: ts.Node, project: string, external: boolean): void {\n const info = external\n ? externalApiInfoFrom(node, project)\n : apiClassInfoFromNode(node, project, this.diagnostics);\n if (info) {\n this.owners.add(project);\n this.byName.set(info.api, info);\n }\n ts.forEachChild(node, (child: ts.Node) => this.indexNode(child, project, external));\n }\n}\n/** Per-owner accumulator that dedupes API refs while a single project is scanned. */\nclass RelationAccumulator {\n private readonly implementsByOwner = new Map<string, Map<string, ApiRef>>();\n private readonly usesByOwner = new Map<string, Map<string, ApiRef>>();\n\n addImplements(owner: string, ref: ApiRef): void {\n ensureRefMap(this.implementsByOwner, owner).set(apiRefKey(ref), ref);\n }\n\n /**\n * Keyed by api + targetService: one project legitimately binds the SAME contract against two\n * different services (a WarmupApi client per data server), and those are two relations, not one.\n */\n addUses(owner: string, ref: ApiRef): void {\n ensureRefMap(this.usesByOwner, owner).set(apiRefKey(ref), ref);\n }\n\n /** Build the deterministic { owner -> relation } record, owners in sorted order. */\n toRelations(): ProjectApiRelations {\n const owners = new Set<string>([\n ...this.implementsByOwner.keys(),\n ...this.usesByOwner.keys(),\n ]);\n const relations: ProjectApiRelations = {};\n for (const owner of [...owners].sort()) {\n const implementsRefs = sortApiRefs([\n ...(this.implementsByOwner.get(owner)?.values() ?? []),\n ]);\n const usesRefs = sortApiRefs([...(this.usesByOwner.get(owner)?.values() ?? [])]);\n const relation: ApiRelation = {\n kind: deriveApiRelationKind(implementsRefs, usesRefs),\n implements: implementsRefs,\n uses: usesRefs,\n };\n relations[owner] = relation;\n }\n return relations;\n }\n\n isEmpty(): boolean {\n return this.implementsByOwner.size === 0 && this.usesByOwner.size === 0;\n }\n}\n\n// webpieces-disable no-function-outside-class -- tiny map helper, matching the AST-helper style of di-graph/bindings.ts\nfunction ensureRefMap(map: Map<string, Map<string, ApiRef>>, owner: string): Map<string, ApiRef> {\n let inner = map.get(owner);\n if (!inner) {\n inner = new Map<string, ApiRef>();\n map.set(owner, inner);\n }\n return inner;\n}\n\n/** Statically scans every project for its api-lib implements/uses relationships. */\nexport class ApiUsageScanner {\n private readonly locator: ProjectLocator;\n private readonly relationsByProject = new Map<string, ProjectApiRelations>();\n private readonly scannedProjects = new Set<string>();\n private readonly unresolvedApiCalls: UnresolvedApiCall[] = [];\n private readonly decoratorArgDiagnostics: DecoratorArgDiagnostics;\n private sourceIndex = new ApiSourceIndex(new Map<string, ApiClassInfo>(), new Set<string>());\n\n constructor(\n private readonly workspaceRoot: string,\n private readonly projectInfos: Map<string, ProjectInfo>,\n /** Globs of project roots whose exported `*Api` types are contracts for outside systems. */\n private readonly externalApiPaths: readonly string[] = [],\n /** `no-root-union-api-type`'s switches — ARMED unless scanAndAttachApiRelations read otherwise. */\n private readonly rootUnionRule: RootUnionRule = RootUnionRule.enabledEverywhere(),\n /** `api-rules-for-openapi`'s switches — read from the config, which MUST state them (#1017). */\n private readonly openApiRule: ApiDocRule = ApiDocRule.off(OPENAPI_RULE),\n /** `api-rules-for-mcp`'s switches — read from the config, which MUST state them (#1017). */\n private readonly mcpRule: ApiDocRule = ApiDocRule.off(MCP_RULE),\n ) {\n this.locator = new ProjectLocator(workspaceRoot, projectInfos);\n this.decoratorArgDiagnostics = new DecoratorArgDiagnostics(workspaceRoot);\n }\n\n scan(): ApiScanResult {\n // Pre-pass: every contract, from source, BEFORE any call site is resolved — a call site in\n // one project routinely names a contract owned by a project we have not walked yet.\n this.sourceIndex = new ApiSourceIndexBuilder(\n this.workspaceRoot,\n this.projectInfos,\n this.externalApiPaths,\n this.decoratorArgDiagnostics,\n ).build();\n for (const info of this.projectInfos.values()) {\n if (info.root === '' || info.root === '.') continue;\n this.scanProject(info);\n }\n return {\n relationsByProject: this.relationsByProject,\n apiLibProjects: this.sourceIndex.owners,\n apiIndex: this.sourceIndex.byName,\n scannedProjects: this.scannedProjects,\n unresolvedApiCalls: this.unresolvedApiCalls,\n nonLiteralDecoratorArgs: this.decoratorArgDiagnostics.all(),\n unresolvedEndpointPaths: this.decoratorArgDiagnostics.unresolvedEndpointPaths(),\n emptiedApiContracts: this.decoratorArgDiagnostics.emptiedContracts(),\n undeclaredExternalCallers: this.decoratorArgDiagnostics.undeclaredExternalCallers(),\n undeclaredEndpointOperations:\n this.decoratorArgDiagnostics.undeclaredEndpointOperations(),\n rootUnions: new RootUnionScan(\n this.workspaceRoot,\n this.projectInfos,\n this.rootUnionRule,\n ).run(),\n apiDocRules: new ApiDocRulesScan(\n this.workspaceRoot,\n this.projectInfos,\n this.openApiRule,\n this.mcpRule,\n ).run(),\n };\n }\n\n private scanProject(info: ProjectInfo): void {\n const program = createScanProgram(path.resolve(this.workspaceRoot, info.root));\n if (!program) return;\n const checker = program.getTypeChecker();\n const accumulator = new RelationAccumulator();\n let scannedProductionFile = false;\n\n for (const sourceFile of program.getSourceFiles()) {\n if (sourceFile.isDeclarationFile || sourceFile.fileName.includes('/node_modules/'))\n continue;\n if (isTestFile(sourceFile.fileName)) continue; // tests are not production topology\n // Only this project's OWN files — imported api-lib source is in the program too.\n if (this.locator.projectOf(sourceFile.fileName) !== info.name) continue;\n scannedProductionFile = true;\n this.visit(sourceFile, checker, info.name, accumulator);\n }\n\n // Record coverage only when we actually saw production source — an all-test project (e2e)\n // stays absent so the validator won't wrongly flag its api-lib deps as unused.\n if (scannedProductionFile) this.scannedProjects.add(info.name);\n if (!accumulator.isEmpty())\n this.relationsByProject.set(info.name, accumulator.toRelations());\n }\n\n private visit(\n node: ts.Node,\n checker: ts.TypeChecker,\n project: string,\n acc: RelationAccumulator,\n ): void {\n // In-repo contract classes are indexed by the source pre-pass, so only calls matter for them.\n if (ts.isCallExpression(node)) this.recordCall(node, checker, project, acc);\n // A VENDOR contract has no client-factory call site to key off — it arrives by injection —\n // so classes have to be inspected too.\n if (ts.isClassDeclaration(node)) this.recordExternalUses(node, acc);\n ts.forEachChild(node, (child: ts.Node) => this.visit(child, checker, project, acc));\n }\n\n /**\n * Record a `uses` for every vendor contract this class receives by CONSTRUCTOR INJECTION —\n * `constructor(@inject(GMAIL_TYPES.GmailApi) private readonly gmail: GmailApi)`.\n *\n * The parameter TYPE is the signal, not the token: a token is an opaque Symbol whose name we\n * would have to guess at, while the type is written right there and is what the class actually\n * calls. Matching happens by name against the external index, so an import that resolves to a\n * built `.d.ts` works exactly as well as one resolving to source.\n *\n * A class that IMPLEMENTS the contract is skipped — that is the vendor adapter (`GmailClient`)\n * or a test double (`InMemoryFirestore`, `MockTts`), which IS the seam rather than a caller of\n * it. Counting those would draw an edge from every service embedding a fake to a vendor it never\n * actually reaches.\n */\n private recordExternalUses(cls: ts.ClassDeclaration, acc: RelationAccumulator): void {\n const implemented = implementedTypeNames(cls);\n for (const param of constructorParamsOf(cls)) {\n const typeName = typeReferenceName(param.type);\n if (typeName === null || implemented.has(typeName)) continue;\n const info = this.sourceIndex.lookup(typeName);\n if (info === null || info.type !== 'external') continue;\n acc.addUses(info.owner, { api: info.api, type: 'external' });\n }\n }\n\n private recordCall(\n call: ts.CallExpression,\n checker: ts.TypeChecker,\n project: string,\n acc: RelationAccumulator,\n ): void {\n const method = calleeMethodName(call);\n if (method === null || call.arguments.length === 0) return;\n if (method === ADD_ROUTES_METHOD) {\n const info = this.apiInfoFromExpr(call.arguments[0], checker, project);\n if (info) acc.addImplements(info.owner, { api: info.api, type: info.type });\n return;\n }\n if (method === RPC_CLIENT_METHOD || method === PUBSUB_CLIENT_METHOD) {\n const info = this.apiInfoFromExpr(call.arguments[0], checker, project);\n if (!info) return;\n // Argument 2 names WHICH service this client talks to. Keeping it is what lets the\n // runtime graph draw ONE edge instead of one per implementer of the contract.\n const targetService = targetServiceOf(call);\n const ref: ApiRef = { api: info.api, type: info.type };\n if (targetService !== null) ref.targetService = targetService;\n acc.addUses(info.owner, ref);\n }\n }\n\n /** Resolve an expression to the API contract it names, or null if it is not one. */\n private apiInfoFromExpr(\n expr: ts.Expression,\n checker: ts.TypeChecker,\n project: string,\n ): ApiClassInfo | null {\n const decl = resolveClassDeclaration(expr, checker);\n if (!decl) return null;\n const fromSource = this.apiClassInfoFor(decl);\n return fromSource ?? this.recoverFromDeclaration(decl, expr, project);\n }\n\n /**\n * The checker landed on a BUILT declaration instead of source — the consumer has no\n * tsconfig.base `paths` entry for the api-lib, so the import went through node_modules to\n * `dist/**.d.ts`. tsc erases decorators when emitting declarations, so `@ApiPath` is simply\n * not there and never will be. Recover the contract by name from the source index; the graph\n * is then correct no matter how the consumer's tsconfig is laid out.\n */\n private recoverFromDeclaration(\n decl: ts.ClassDeclaration,\n expr: ts.Expression,\n project: string,\n ): ApiClassInfo | null {\n // An abstract class is the shape of a contract; a non-abstract argument is genuinely not one.\n if (!decl.getSourceFile().isDeclarationFile || !isAbstractClass(decl) || !decl.name)\n return null;\n const recovered = this.sourceIndex.lookup(decl.name.text);\n if (recovered) return recovered;\n // Abstract, in a .d.ts, yet no workspace source owns it — the scan is blind here. Say so.\n this.unresolvedApiCalls.push(\n new UnresolvedApiCall(\n project,\n decl.name.text,\n this.relativeLocation(expr),\n this.relativePath(decl.getSourceFile().fileName),\n ),\n );\n return null;\n }\n\n /** `path/to/file.ts:LINE` for `node`, workspace-relative, for a human-readable report. */\n private relativeLocation(node: ts.Node): string {\n const sourceFile = node.getSourceFile();\n const position = sourceFile.getLineAndCharacterOfPosition(node.getStart());\n return `${this.relativePath(sourceFile.fileName)}:${position.line + 1}`;\n }\n\n private relativePath(absFile: string): string {\n return path.relative(this.workspaceRoot, absFile);\n }\n\n /**\n * {api, owner, type, methods} when `cls` is an `abstract class` carrying `@ApiPath` IN SOURCE,\n * else null. Only the OWNER differs from the index pre-pass — here it comes from the file's\n * location rather than from the project being walked — so the contract test itself is delegated\n * to apiClassInfoFrom, keeping one definition of \"this is a contract\".\n */\n private apiClassInfoFor(cls: ts.ClassDeclaration): ApiClassInfo | null {\n const owner = this.locator.projectOf(cls.getSourceFile().fileName);\n if (owner === null) return null;\n return apiClassInfoFrom(cls, owner);\n }\n}\n\n/**\n * Run the scan and attach the derived `apiRelations` onto each graph entry in\n * place. Shared by `architecture:generate` (which then saves) and\n * `architecture:validate-architecture-unchanged` (which regenerates in memory\n * and must attach the SAME field, or it would see a phantom diff). Returns the\n * full scan so callers (validators, runtime graph) can reuse the api index.\n */\n// webpieces-disable no-function-outside-class -- module entry point, mirrors generateReducedGraph/collectBindings\nexport function scanAndAttachApiRelations(\n workspaceRoot: string,\n graph: EnhancedGraph,\n projectInfos: Map<string, ProjectInfo>,\n externalApiPaths: readonly string[] = [],\n): ApiScanResult {\n const result = new ApiUsageScanner(\n workspaceRoot,\n projectInfos,\n externalApiPaths,\n RootUnionRule.fromConfig(workspaceRoot),\n ApiDocRule.fromConfig(workspaceRoot, OPENAPI_RULE),\n ApiDocRule.fromConfig(workspaceRoot, MCP_RULE),\n ).scan();\n for (const projectName of result.relationsByProject.keys()) {\n const entry = graph[projectName];\n if (entry) entry.apiRelations = result.relationsByProject.get(projectName);\n }\n return result;\n}\n\n/**\n * The committed api contract table (one `architecture/apis/<ApiName>.json` per entry), from a\n * completed scan.\n *\n * Only contracts with ≥1 endpoint are emitted: a vendor seam has no routes, so a table entry for it\n * would be an empty shell, and its identity is already carried by the `external` refs in\n * apiRelations. Sorted by api name, methods left in declaration order, so the file is deterministic.\n *\n * THROWS on the ways an entry can be wrong-but-green, checked root cause first:\n * 1. an `@Endpoint` path the scan could not read (UnresolvedEndpointPathError) — the other half of\n * the URL a consumer computes, and the cause of most emptied contracts;\n * 2. a class that declared endpoints and kept none (EmptiedApiContractError), which would otherwise\n * leave silently through the zero-method skip above;\n * 3. a method without an explicit operation (UndeclaredEndpointOperationError);\n * 4. an `external` method that never said WHO calls it (UndeclaredExternalCallerError);\n * 5. a routed contract with no basePath (MissingBasePathError).\n * All are worse than an absent entry: a consumer joining `basePath + path` computes a\n * confidently wrong URL with no signal that anything is off, because every other entry is complete.\n * Each error aggregates EVERY offender, so a developer fixing five constants sees five in one run.\n */\n// webpieces-disable no-function-outside-class -- module entry point, mirrors scanAndAttachApiRelations\nexport function buildApiContracts(scan: ApiScanResult): ApiContracts {\n // Root cause before symptom: an unreadable path is what empties a contract, so naming the paths\n // is what the author can actually act on.\n if (scan.unresolvedEndpointPaths.length > 0)\n throw new UnresolvedEndpointPathError(scan.unresolvedEndpointPaths);\n if (scan.emptiedApiContracts.length > 0)\n throw new EmptiedApiContractError(scan.emptiedApiContracts);\n if (scan.undeclaredEndpointOperations.length > 0) {\n throw new UndeclaredEndpointOperationError(scan.undeclaredEndpointOperations);\n }\n // A shape no function-calling API will accept, read perfectly well — unlike the four above, which\n // are contracts the scan could not READ at all.\n if (!scan.rootUnions.isEmpty()) throw new RootUnionApiTypeError(scan.rootUnions);\n // Contract shapes that are not PUBLISHABLE, read by the generator's own extractor (#1011).\n // After the root union, which is the one shape no function-calling API will accept at all, and\n // before the caller checks below, which are about the architecture graph rather than a document.\n if (!scan.apiDocRules.openApi.isEmpty()) {\n throw new ApiRulesForOpenApiError(scan.apiDocRules.openApi);\n }\n if (!scan.apiDocRules.mcp.isEmpty()) {\n throw new ApiRulesForMcpError(scan.apiDocRules.mcp, scan.apiDocRules.mcpExclusions);\n }\n // After the two above: an unreadable path is what empties a contract, and a contract that lost\n // every method has no external endpoint left to complain about.\n if (scan.undeclaredExternalCallers.length > 0) {\n throw new UndeclaredExternalCallerError(scan.undeclaredExternalCallers);\n }\n const contracts: ApiContracts = {};\n const missing: string[] = [];\n for (const api of [...scan.apiIndex.keys()].sort()) {\n const info = scan.apiIndex.get(api)!;\n if (info.methods.length === 0) continue;\n if (info.basePath === undefined) {\n missing.push(`${api} (owner ${info.owner})`);\n continue;\n }\n const contract: ApiContract = {\n owner: info.owner,\n apiKind: info.type,\n basePath: info.basePath,\n methods: info.methods,\n };\n contracts[api] = contract;\n }\n if (missing.length > 0) throw new MissingBasePathError(missing);\n return contracts;\n}\n\n/**\n * Loud, actionable report for decorator arguments the scan could not reduce to a string.\n *\n * Same-module constants resolve, so anything reaching here is genuinely out of reach of a\n * parser-only pass — and every one of them silently shrinks the graph. Empty string when there is\n * nothing to say, so callers can test it without special-casing.\n */\n// webpieces-disable no-function-outside-class -- pure formatter, mirrors describeUnresolvedApiCalls\nexport function describeNonLiteralDecoratorArgs(args: readonly NonLiteralDecoratorArg[]): string {\n if (args.length === 0) return '';\n const lines = [\n `⚠️ ${args.length} decorator argument(s) are not string literals and could not be resolved.`,\n ` Each one drops data from the graph: a missing basePath, a missing method, or a whole contract:`,\n ];\n for (const arg of args) {\n const where = arg.method === null ? arg.api : `${arg.api}.${arg.method}`;\n lines.push(` • @${arg.decorator}(${arg.argument}) on ${where} at ${arg.at}`);\n }\n lines.push(\n ` A constant declared in the SAME module resolves. One imported from another module does not —`,\n ` this scan is parser-only by design (module resolution can land on a decorator-erased .d.ts).`,\n ` Fix by inlining the string literal, or by moving the constant into the contract's own module.`,\n );\n return lines.join('\\n');\n}\n\n/**\n * Every contract method whose declared @Endpoint kind its api kind cannot deliver — an rpc method on\n * a @PubSub contract (nothing calls a queue synchronously), or a cloudtasks/cron method on an @Rpc\n * contract (naming a queue or schedule nothing could deliver to). Mirrors core-util's\n * ENDPOINT_KINDS_BY_API_KIND at BUILD time, where it can name the file instead of throwing at wiring.\n */\n// webpieces-disable no-function-outside-class -- pure formatter, mirrors describeUnresolvedApiCalls\nexport function describeMismatchedEndpointKinds(contracts: ApiContracts): string[] {\n const allowedByKind: Record<string, readonly EndpointKind[]> = {\n rpc: ['rpc', 'external'],\n pubsub: ['cloudtasks', 'cron', 'external'],\n };\n const problems: string[] = [];\n for (const api of Object.keys(contracts)) {\n const contract = contracts[api];\n const allowed = allowedByKind[contract.apiKind];\n if (allowed === undefined) continue;\n for (const method of contract.methods) {\n if (allowed.includes(method.kind)) continue;\n problems.push(\n `${api}.${method.name} declares @Endpoint(${method.httpMethod ?? 'POST'}, '${method.path}', ${method.operation}, ${method.kind}) but ${api} is ` +\n `@${contract.apiKind === 'pubsub' ? 'PubSub' : 'Rpc'} — allowed kinds are ${allowed.join(' | ')}.`,\n );\n }\n }\n return problems;\n}\n\n/**\n * Build a program for scanning ONE project. Prefers the project's compile tsconfig; but when that\n * is a solution-style tsconfig (only `references`, no `files`/`include` — e.g. legacy-server), it\n * yields zero files, so we fall back to globbing the project's own `src/**` and reuse the resolved\n * compiler options (which carry tsconfig.base `paths` for cross-package @webpieces resolution).\n *\n * `paths` is a PREFERENCE, not a precondition: it lets imports resolve straight to source. Without\n * it they land on a decorator-erased `dist/**.d.ts`, which the source index recovers from — see\n * ApiUsageScanner.recoverFromDeclaration.\n */\n// webpieces-disable no-function-outside-class -- ts Program factory, mirrors di-graph/program.ts\nfunction createScanProgram(projectRootAbs: string): ts.Program | null {\n const configPath = findProjectTsconfig(projectRootAbs);\n if (!configPath) return buildProgramFromSrc(projectRootAbs, {});\n const host = Object.assign({}, ts.sys, {\n onUnRecoverableConfigFileDiagnostic: (): void => undefined,\n }) as ts.ParseConfigFileHost;\n const parsed = ts.getParsedCommandLineOfConfigFile(configPath, {}, host);\n if (!parsed) return null;\n if (parsed.fileNames.length > 0) return ts.createProgram(parsed.fileNames, parsed.options);\n return buildProgramFromSrc(projectRootAbs, parsed.options);\n}\n\n// webpieces-disable no-function-outside-class -- ts Program factory helper, mirrors di-graph/program.ts\nfunction buildProgramFromSrc(\n projectRootAbs: string,\n options: ts.CompilerOptions,\n): ts.Program | null {\n const srcDir = path.join(projectRootAbs, 'src');\n if (!fs.existsSync(srcDir)) return null;\n const files = collectTsFiles(srcDir);\n return files.length > 0 ? ts.createProgram(files, options) : null;\n}\n"]}
|