@webpieces/nx-webpieces-rules 0.4.808 → 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 -6
- package/src/executors/generate/executor.js +5 -0
- package/src/executors/generate/executor.js.map +1 -1
- package/src/lib/api-usage/api-contract-errors.d.ts +59 -0
- package/src/lib/api-usage/api-contract-errors.js +129 -1
- package/src/lib/api-usage/api-contract-errors.js.map +1 -1
- package/src/lib/api-usage/api-doc-rules-scan.d.ts +115 -0
- package/src/lib/api-usage/api-doc-rules-scan.js +372 -0
- package/src/lib/api-usage/api-doc-rules-scan.js.map +1 -0
- package/src/lib/api-usage/api-doc-rules-verdicts.d.ts +109 -0
- package/src/lib/api-usage/api-doc-rules-verdicts.js +323 -0
- package/src/lib/api-usage/api-doc-rules-verdicts.js.map +1 -0
- package/src/lib/api-usage/api-doc-rules.d.ts +186 -0
- package/src/lib/api-usage/api-doc-rules.js +298 -0
- package/src/lib/api-usage/api-doc-rules.js.map +1 -0
- package/src/lib/api-usage/api-scanner.d.ts +15 -1
- package/src/lib/api-usage/api-scanner.js +22 -2
- package/src/lib/api-usage/api-scanner.js.map +1 -1
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `api-rules-for-openapi` and `api-rules-for-mcp` (#1011) — the CI half of "is this contract
|
|
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.
|
|
10
|
+
*
|
|
11
|
+
* ## The acceptance contract, and why this file drives the generator instead of copying it
|
|
12
|
+
*
|
|
13
|
+
* The rules exist so that ADDING `@ApiType(...)` (and `@WpMcpTool`) to a contract that passes them
|
|
14
|
+
* always works. That is only true if "expressible" has exactly ONE definition, so this scan runs the
|
|
15
|
+
* generator's own code — `ApiDocExtractor` / `TypeResolver` from `@webpieces/api-doc-model` for the
|
|
16
|
+
* OpenAPI half, and `McpSchemaRenderer` tool-by-tool for the MCP half. A second implementation of
|
|
17
|
+
* "what can be published" would drift from the first on the release that improved either one, and
|
|
18
|
+
* the drift would be silent: the rules would stay green while generation started failing. The two
|
|
19
|
+
* packages ship on the same release train, so the dependency is in lockstep by construction.
|
|
20
|
+
*
|
|
21
|
+
* Two things here are STRICTER than the generator, deliberately, and both are publishing rules
|
|
22
|
+
* rather than expressibility ones (being stricter cannot break the acceptance contract — it can only
|
|
23
|
+
* refuse something that would have generated):
|
|
24
|
+
*
|
|
25
|
+
* - an `unknown` VALUE TYPE anywhere (`Record<string, unknown>`, `unknown[]`, a bare `unknown`
|
|
26
|
+
* field). The extractor maps it to a primitive and the generator publishes `{}`, which in JSON
|
|
27
|
+
* Schema means "anything" — a partner-facing field with no shape, which is the defect the
|
|
28
|
+
* unmapped guard exists for, arriving through a door the guard does not watch.
|
|
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).
|
|
34
|
+
*
|
|
35
|
+
* ## Why it lives in the rules engine and not in the doc parser
|
|
36
|
+
*
|
|
37
|
+
* `@webpieces/api-doc-model` is only ever pointed at contracts somebody chose to publish. `@ApiType`
|
|
38
|
+
* is a PUBLISHING decision added later, on purpose — so a shape rule that only ran on contracts which
|
|
39
|
+
* had already opted in would let a team discover, six months afterwards, that the type was never
|
|
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.
|
|
42
|
+
*
|
|
43
|
+
* ## Root-level unions are NOT re-checked here
|
|
44
|
+
*
|
|
45
|
+
* `no-root-union-api-type` (#1009) already refuses them, workspace-wide, with its own config key and
|
|
46
|
+
* its own per-site hatch. One implementation. The MCP half still reports one when it meets it,
|
|
47
|
+
* because `McpSchemaRenderer` refuses it as its own backstop and this scan reports whatever the
|
|
48
|
+
* renderer says — which is the correct division: the OpenAPI document publishes a root union
|
|
49
|
+
* perfectly well, and only a tool schema cannot carry one.
|
|
50
|
+
*/
|
|
51
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
52
|
+
exports.ApiDocRulesScan = void 0;
|
|
53
|
+
const tslib_1 = require("tslib");
|
|
54
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
55
|
+
const path = tslib_1.__importStar(require("path"));
|
|
56
|
+
const ts = tslib_1.__importStar(require("typescript"));
|
|
57
|
+
const api_doc_model_1 = require("@webpieces/api-doc-model");
|
|
58
|
+
const api_ast_1 = require("./api-ast");
|
|
59
|
+
const api_doc_rules_1 = require("./api-doc-rules");
|
|
60
|
+
const api_doc_rules_verdicts_1 = require("./api-doc-rules-verdicts");
|
|
61
|
+
/** `@ApiPath(` at COLUMN ZERO — a docstring that TALKS about a contract declares none. */
|
|
62
|
+
const DECLARES_CONTRACT = /^@ApiPath\(/m;
|
|
63
|
+
/** ONE contract file, and which of the two rules apply to the project that owns it. */
|
|
64
|
+
class ContractFile {
|
|
65
|
+
absPath;
|
|
66
|
+
openApi;
|
|
67
|
+
mcp;
|
|
68
|
+
constructor(absPath, openApi, mcp) {
|
|
69
|
+
this.absPath = absPath;
|
|
70
|
+
this.openApi = openApi;
|
|
71
|
+
this.mcp = mcp;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Where one declaration sits, already split out of the extractor's `File.ts:LINE:COL` spelling. */
|
|
75
|
+
class Site {
|
|
76
|
+
absPath;
|
|
77
|
+
line;
|
|
78
|
+
constructor(absPath, line) {
|
|
79
|
+
this.absPath = absPath;
|
|
80
|
+
this.line = line;
|
|
81
|
+
}
|
|
82
|
+
/** `path/to/File.ts:LINE`, workspace-relative — what a refusal prints. */
|
|
83
|
+
relativeTo(workspaceRoot) {
|
|
84
|
+
return `${path.relative(workspaceRoot, this.absPath)}:${this.line}`;
|
|
85
|
+
}
|
|
86
|
+
/** `abs/File.ts:12:5` -> a Site. An unparseable one falls back to line 1 of `fallback`. */
|
|
87
|
+
// webpieces-disable no-function-outside-class -- static factory of this class
|
|
88
|
+
static parse(location, fallback) {
|
|
89
|
+
const match = location.match(/^(.*):(\d+):\d+$/);
|
|
90
|
+
if (match === null)
|
|
91
|
+
return new Site(fallback, 1);
|
|
92
|
+
return new Site(match[1], Number(match[2]));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* ONE rule's accumulator. It is what applies the per-site disable, so the disable semantics live in
|
|
97
|
+
* exactly one place and cannot differ between the two rules.
|
|
98
|
+
*/
|
|
99
|
+
class DefectCollector {
|
|
100
|
+
ruleName;
|
|
101
|
+
violations = [];
|
|
102
|
+
reasonless = [];
|
|
103
|
+
constructor(ruleName) {
|
|
104
|
+
this.ruleName = ruleName;
|
|
105
|
+
}
|
|
106
|
+
/** The rule this collector reports under — what a shared defect's cure has to name. */
|
|
107
|
+
rule() {
|
|
108
|
+
return this.ruleName;
|
|
109
|
+
}
|
|
110
|
+
add(defect, site) {
|
|
111
|
+
const disable = api_doc_rules_1.DisableComment.readAt(site.absPath, site.line, this.ruleName);
|
|
112
|
+
if (disable === undefined) {
|
|
113
|
+
this.violations.push(defect);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (!disable.hasReason)
|
|
117
|
+
this.reasonless.push(defect);
|
|
118
|
+
}
|
|
119
|
+
findings() {
|
|
120
|
+
return new api_doc_rules_1.ApiRuleFindings(DefectCollector.externalFirst(this.violations), DefectCollector.externalFirst(this.reasonless));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Partner-facing contracts first. The same defect is a different size depending on who reads the
|
|
124
|
+
* document it would have been in, and a list that buries the `external-customer` ones among
|
|
125
|
+
* thirty internal ones has hidden the only urgent line in it.
|
|
126
|
+
*/
|
|
127
|
+
// webpieces-disable no-function-outside-class -- private static ordering of this class
|
|
128
|
+
static externalFirst(found) {
|
|
129
|
+
return [...found].sort((a, b) => Number(b.isExternal()) - Number(a.isExternal()));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Routes a defect to the rule that owns it.
|
|
134
|
+
*
|
|
135
|
+
* Every OpenAPI-level defect ALSO blocks MCP, so it is reported by whichever rule is running —
|
|
136
|
+
* `api-rules-for-openapi` when that one is on, and `api-rules-for-mcp` alone when it is not. It is
|
|
137
|
+
* never reported twice: a team running both would otherwise read every shared defect in two places
|
|
138
|
+
* and have to work out that they are one.
|
|
139
|
+
*/
|
|
140
|
+
class DefectSink {
|
|
141
|
+
openApi;
|
|
142
|
+
mcp;
|
|
143
|
+
constructor(openApi, mcp) {
|
|
144
|
+
this.openApi = openApi;
|
|
145
|
+
this.mcp = mcp;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* A defect that blocks the OpenAPI document, and therefore every tool on it too.
|
|
149
|
+
*
|
|
150
|
+
* The defect is BUILT from the rule that ends up reporting it, not handed in ready-made, because
|
|
151
|
+
* a shared defect does not know in advance which rule will carry it: `api-rules-for-openapi` when
|
|
152
|
+
* that one runs, and `api-rules-for-mcp` alone when it does not. A cure that named a fixed rule
|
|
153
|
+
* would, on the mcp-only configuration, prescribe a `// webpieces-disable` line the collector
|
|
154
|
+
* reading that site does not look for.
|
|
155
|
+
*/
|
|
156
|
+
shared(build, site) {
|
|
157
|
+
const target = this.openApi ?? this.mcp;
|
|
158
|
+
if (target === undefined)
|
|
159
|
+
return;
|
|
160
|
+
target.add(build(target.rule()), site);
|
|
161
|
+
}
|
|
162
|
+
/** A defect that blocks ONE tool and nothing else. */
|
|
163
|
+
mcpOnly(defect, site) {
|
|
164
|
+
this.mcp?.add(defect, site);
|
|
165
|
+
}
|
|
166
|
+
anyRuleRuns() {
|
|
167
|
+
return this.openApi !== undefined || this.mcp !== undefined;
|
|
168
|
+
}
|
|
169
|
+
/** True when `api-rules-for-mcp` applies to this file — what the exclusion list is scoped to. */
|
|
170
|
+
mcpRuns() {
|
|
171
|
+
return this.mcp !== undefined;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Walks every project's `src`, extracts every `@ApiPath` contract with the generator's own
|
|
176
|
+
* extractor, and judges the result against the two rules.
|
|
177
|
+
*
|
|
178
|
+
* ONE `ts.Program` over every contract file in the workspace, because a DTO a contract reaches
|
|
179
|
+
* routinely lives in another project and the checker has to be able to follow the import — the same
|
|
180
|
+
* reason the repo sweep in `@webpieces/api-doc-model`'s own spec builds one program rather than one
|
|
181
|
+
* per file.
|
|
182
|
+
*/
|
|
183
|
+
class ApiDocRulesScan {
|
|
184
|
+
workspaceRoot;
|
|
185
|
+
projectInfos;
|
|
186
|
+
openApiRule;
|
|
187
|
+
mcpRule;
|
|
188
|
+
/**
|
|
189
|
+
* Every `@InvalidEndpointForMcp` endpoint met on a file the MCP rule applies to.
|
|
190
|
+
*
|
|
191
|
+
* Collected even on a run with no findings at all, because restating them IS the feature: the
|
|
192
|
+
* alternative considered in #1014 was a one-off warning when somebody adds one, and a warning
|
|
193
|
+
* printed once at the moment of the decision is read by the one person who already knows.
|
|
194
|
+
*/
|
|
195
|
+
exclusions = [];
|
|
196
|
+
constructor(workspaceRoot, projectInfos,
|
|
197
|
+
/** OFF unless a caller read otherwise out of webpieces.config.json, which MUST state it. */
|
|
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)) {
|
|
199
|
+
this.workspaceRoot = workspaceRoot;
|
|
200
|
+
this.projectInfos = projectInfos;
|
|
201
|
+
this.openApiRule = openApiRule;
|
|
202
|
+
this.mcpRule = mcpRule;
|
|
203
|
+
}
|
|
204
|
+
run() {
|
|
205
|
+
if (!this.openApiRule.enabled && !this.mcpRule.enabled)
|
|
206
|
+
return api_doc_rules_1.ApiDocRulesFindings.empty();
|
|
207
|
+
const files = this.contractFiles();
|
|
208
|
+
if (files.length === 0)
|
|
209
|
+
return api_doc_rules_1.ApiDocRulesFindings.empty();
|
|
210
|
+
const openApi = this.openApiRule.enabled ? new DefectCollector(api_doc_rules_1.OPENAPI_RULE) : undefined;
|
|
211
|
+
const mcp = this.mcpRule.enabled ? new DefectCollector(api_doc_rules_1.MCP_RULE) : undefined;
|
|
212
|
+
const program = ts.createProgram(files.map((file) => file.absPath), this.compilerOptions());
|
|
213
|
+
const toolNames = new Map();
|
|
214
|
+
for (const file of files) {
|
|
215
|
+
const sink = new DefectSink(file.openApi ? openApi : undefined, file.mcp ? mcp : undefined);
|
|
216
|
+
this.judgeFile(file, program, sink, toolNames);
|
|
217
|
+
}
|
|
218
|
+
return new api_doc_rules_1.ApiDocRulesFindings(openApi?.findings() ?? new api_doc_rules_1.ApiRuleFindings([], []), mcp?.findings() ?? new api_doc_rules_1.ApiRuleFindings([], []), this.exclusions);
|
|
219
|
+
}
|
|
220
|
+
/** Every contract in one file, or the ONE refusal that stopped the file being read at all. */
|
|
221
|
+
judgeFile(file, program, sink, toolNames) {
|
|
222
|
+
if (!sink.anyRuleRuns())
|
|
223
|
+
return;
|
|
224
|
+
const source = program.getSourceFile(file.absPath);
|
|
225
|
+
if (source === undefined)
|
|
226
|
+
return;
|
|
227
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- an extraction refusal IS a finding; it is reported, not propagated
|
|
228
|
+
try {
|
|
229
|
+
for (const model of new api_doc_model_1.ApiDocExtractor().extractAllFrom(program, source)) {
|
|
230
|
+
this.judgeModel(model, source, sink, toolNames);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
//const error = toError(err);
|
|
235
|
+
if (!(err instanceof api_doc_model_1.ApiDocExtractionError))
|
|
236
|
+
throw err;
|
|
237
|
+
this.reportExtractionFailure(err, file, source, sink);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* An extraction that REFUSED. Reported under the shared list because it stops BOTH documents:
|
|
242
|
+
* `@Endpoint` arguments that cannot be constant-folded, a bound on a non-numeric field, and the
|
|
243
|
+
* `@ApiType(..., MCP)` ⇔ `@WpMcpTool` biconditional all fail here, before a model exists.
|
|
244
|
+
*/
|
|
245
|
+
reportExtractionFailure(error, file, source, sink) {
|
|
246
|
+
const site = Site.parse(error.location, file.absPath);
|
|
247
|
+
sink.shared(() => new api_doc_rules_1.ApiContractDefect((0, api_doc_rules_verdicts_1.contractNamesIn)(source)[0] ?? path.basename(file.absPath), '', `the contract cannot be read at all — ${error.message}`, site.relativeTo(this.workspaceRoot), error.cure, []), site);
|
|
248
|
+
}
|
|
249
|
+
/** ONE contract: the shared OpenAPI checks, then the MCP-only ones. */
|
|
250
|
+
judgeModel(model, source, sink, toolNames) {
|
|
251
|
+
const lines = (0, api_doc_rules_verdicts_1.contractLinesOf)(source, model.contractName);
|
|
252
|
+
this.judgeUnmapped(model, sink, source.fileName);
|
|
253
|
+
this.judgeUnknownValues(model, sink, source.fileName);
|
|
254
|
+
this.judgeAnsweringResponses(model, source, lines, sink);
|
|
255
|
+
this.judgeTools(model, source, lines, sink, toolNames);
|
|
256
|
+
}
|
|
257
|
+
/** Everything `TypeResolver` could not represent — the generator's OWN verdict, re-worded. */
|
|
258
|
+
judgeUnmapped(model, sink, fallback) {
|
|
259
|
+
for (const unmapped of model.unmapped) {
|
|
260
|
+
const site = Site.parse(unmapped.location, fallback);
|
|
261
|
+
const verdict = (0, api_doc_rules_verdicts_1.classifyUnmapped)(unmapped);
|
|
262
|
+
sink.shared(() => new api_doc_rules_1.ApiContractDefect(model.contractName, '', verdict.what, site.relativeTo(this.workspaceRoot), verdict.cure, model.apiTypes), site);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/** `Record<string, unknown>`, `unknown[]`, `x: unknown` — by VALUE TYPE, never by spelling. */
|
|
266
|
+
judgeUnknownValues(model, sink, fallback) {
|
|
267
|
+
for (const type of model.types.values()) {
|
|
268
|
+
for (const field of type.fields) {
|
|
269
|
+
if (!(0, api_doc_rules_verdicts_1.carriesUnknown)(field.type))
|
|
270
|
+
continue;
|
|
271
|
+
const site = Site.parse(field.location, fallback);
|
|
272
|
+
sink.shared((rule) => new api_doc_rules_1.ApiContractDefect(model.contractName, '', `'${type.name}.${field.name}' publishes an 'unknown' value, so the ` +
|
|
273
|
+
'document states no shape for it at all', site.relativeTo(this.workspaceRoot), (0, api_doc_rules_verdicts_1.unknownValueCure)(rule), model.apiTypes), site);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* An endpoint somebody WAITS ON must NAME a response DTO, even an empty one.
|
|
279
|
+
*
|
|
280
|
+
* `Promise<void>` on a `cloudtasks` or `cron` endpoint is the CONTRACT — fire-and-forget, nothing
|
|
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).
|
|
286
|
+
*/
|
|
287
|
+
judgeAnsweringResponses(model, source, lines, sink) {
|
|
288
|
+
for (const endpoint of model.endpoints) {
|
|
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))
|
|
292
|
+
continue;
|
|
293
|
+
if (endpoint.response !== undefined && !(0, api_doc_rules_verdicts_1.isVoidLike)(endpoint.response))
|
|
294
|
+
continue;
|
|
295
|
+
const site = new Site(source.fileName, lines.lineOf(endpoint.methodName));
|
|
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);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/** Everything `McpToolRegistry` refuses to boot on, plus whatever the renderer cannot render. */
|
|
301
|
+
judgeTools(model, source, lines, sink, toolNames) {
|
|
302
|
+
const renderer = new api_doc_model_1.McpSchemaRenderer(model);
|
|
303
|
+
for (const endpoint of model.endpoints) {
|
|
304
|
+
if (endpoint.invalidForMcp !== undefined) {
|
|
305
|
+
if (!sink.mcpRuns())
|
|
306
|
+
continue;
|
|
307
|
+
// @InvalidEndpointForMcp IS the answer to "could this be a tool". The decorator
|
|
308
|
+
// carries the argument, so the rule asks nothing further and no webpieces-disable is
|
|
309
|
+
// needed — a suppression would be a second, weaker spelling of the same declaration.
|
|
310
|
+
this.exclusions.push(new api_doc_rules_1.McpExclusion(model.contractName, endpoint.methodName, endpoint.invalidForMcp, new Site(source.fileName, lines.lineOf(endpoint.methodName)).relativeTo(this.workspaceRoot)));
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (endpoint.mcpTool === undefined)
|
|
314
|
+
continue;
|
|
315
|
+
const site = new Site(source.fileName, lines.lineOf(endpoint.methodName));
|
|
316
|
+
for (const failure of (0, api_doc_rules_verdicts_1.toolFailures)(endpoint, renderer, toolNames, model)) {
|
|
317
|
+
sink.mcpOnly(new api_doc_rules_1.ApiContractDefect(model.contractName, endpoint.methodName, failure.what, site.relativeTo(this.workspaceRoot), failure.cure, model.apiTypes), site);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
for (const methodName of lines.toolsWithoutEndpoint) {
|
|
321
|
+
const site = new Site(source.fileName, lines.lineOf(methodName));
|
|
322
|
+
sink.mcpOnly(new api_doc_rules_1.ApiContractDefect(model.contractName, methodName, 'carries @WpMcpTool but is not an @Endpoint, so it is not routed at all', site.relativeTo(this.workspaceRoot), "Add @Endpoint(POST, '/path', READ, RPC) to it, or drop the @WpMcpTool.", model.apiTypes), site);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/** Every non-test `.ts` under a project's `src` whose text DECLARES an `@ApiPath` contract. */
|
|
326
|
+
contractFiles() {
|
|
327
|
+
const found = [];
|
|
328
|
+
for (const info of this.projectInfos.values()) {
|
|
329
|
+
if (info.root === '' || info.root === '.')
|
|
330
|
+
continue;
|
|
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);
|
|
336
|
+
if (!openApi && !mcp)
|
|
337
|
+
continue;
|
|
338
|
+
const srcDir = path.join(path.resolve(this.workspaceRoot, info.root), 'src');
|
|
339
|
+
if (!fs.existsSync(srcDir))
|
|
340
|
+
continue;
|
|
341
|
+
for (const file of (0, api_ast_1.collectTsFiles)(srcDir)) {
|
|
342
|
+
if ((0, api_ast_1.isTestFile)(file))
|
|
343
|
+
continue; // a fixture is not a published contract
|
|
344
|
+
if (!DECLARES_CONTRACT.test(fs.readFileSync(file, 'utf8')))
|
|
345
|
+
continue;
|
|
346
|
+
found.push(new ContractFile(file, openApi, mcp));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return found.sort((a, b) => a.absPath.localeCompare(b.absPath));
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* `tsconfig.base.json`'s options when the workspace has one, so an `@webpieces/*` import in a
|
|
353
|
+
* contract RESOLVES and the checker can follow a DTO into another project. Without that the
|
|
354
|
+
* resolver reports every cross-project type as unmapped, which would be a rule failing on its
|
|
355
|
+
* own inability to read rather than on anything the author wrote.
|
|
356
|
+
*/
|
|
357
|
+
compilerOptions() {
|
|
358
|
+
const base = path.join(this.workspaceRoot, 'tsconfig.base.json');
|
|
359
|
+
const declared = fs.existsSync(base)
|
|
360
|
+
? ts.parseJsonConfigFileContent(ts.readConfigFile(base, ts.sys.readFile).config, ts.sys, this.workspaceRoot).options
|
|
361
|
+
: {};
|
|
362
|
+
return {
|
|
363
|
+
...declared,
|
|
364
|
+
noEmit: true,
|
|
365
|
+
skipLibCheck: true,
|
|
366
|
+
types: [],
|
|
367
|
+
experimentalDecorators: true,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
exports.ApiDocRulesScan = ApiDocRulesScan;
|
|
372
|
+
//# sourceMappingURL=api-doc-rules-scan.js.map
|
|
@@ -0,0 +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 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"]}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pure VERDICTS the api-contract rules print, split out of `api-doc-rules-scan.ts`, which owns
|
|
3
|
+
* the walk and is at its file-size limit.
|
|
4
|
+
*
|
|
5
|
+
* Nothing here touches the filesystem or the project graph. Each function answers one question about
|
|
6
|
+
* ONE declaration the extractor or the renderer has already ruled on, and turns it into the two
|
|
7
|
+
* halves every webpieces refusal carries — what is wrong, and what to do instead. The DECISION is
|
|
8
|
+
* never made here: `model.unmapped` is the extractor's and a render failure is the renderer's. What
|
|
9
|
+
* this file adds is the WORDING, which is the part a generic "no model representation for this type
|
|
10
|
+
* form" cannot give an author.
|
|
11
|
+
*/
|
|
12
|
+
import * as ts from 'typescript';
|
|
13
|
+
import { ApiDocModel, DocumentedEndpoint, McpSchemaRenderer, TypeRef, UnmappedType } from '@webpieces/api-doc-model';
|
|
14
|
+
import { EndpointKind } from './api-relations';
|
|
15
|
+
/**
|
|
16
|
+
* The one trigger kind an MCP tool may have. A LITERAL, for the same reason `EndpointKind` beside it
|
|
17
|
+
* is one: this package deliberately does not depend on `@webpieces/core-util` at runtime, and the
|
|
18
|
+
* set the literal belongs to is pinned by that type.
|
|
19
|
+
*/
|
|
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[];
|
|
34
|
+
/** ONE reason a declaration is refused, in the two halves every refusal prints. */
|
|
35
|
+
export declare class Verdict {
|
|
36
|
+
readonly what: string;
|
|
37
|
+
readonly cure: string;
|
|
38
|
+
constructor(what: string, cure: string);
|
|
39
|
+
}
|
|
40
|
+
/** Method name -> the line its declaration starts on, for one contract class. */
|
|
41
|
+
export declare class ContractLines {
|
|
42
|
+
readonly classLine: number;
|
|
43
|
+
readonly byMethod: ReadonlyMap<string, number>;
|
|
44
|
+
/** Methods carrying `@WpMcpTool` that do NOT also carry `@Endpoint`. */
|
|
45
|
+
readonly toolsWithoutEndpoint: readonly string[];
|
|
46
|
+
constructor(classLine: number, byMethod: ReadonlyMap<string, number>,
|
|
47
|
+
/** Methods carrying `@WpMcpTool` that do NOT also carry `@Endpoint`. */
|
|
48
|
+
toolsWithoutEndpoint: readonly string[]);
|
|
49
|
+
lineOf(methodName: string): number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The cure for an `unknown` value, for whichever rule is REPORTING it.
|
|
53
|
+
*
|
|
54
|
+
* It takes the rule name rather than naming one, because this defect is shared: it blocks the
|
|
55
|
+
* OpenAPI document, so `DefectSink` routes it to `api-rules-for-openapi` when that rule is running
|
|
56
|
+
* and to `api-rules-for-mcp` when it is the only one. A hard-coded rule name would then hand an
|
|
57
|
+
* author, on the mcp-only configuration, a `// webpieces-disable api-rules-for-openapi` line that
|
|
58
|
+
* the collector reading the site does not look for — a cure that does not cure, which is the one
|
|
59
|
+
* failure mode a printed cure must not have.
|
|
60
|
+
*/
|
|
61
|
+
export declare function unknownValueCure(rule: string): string;
|
|
62
|
+
/** The one cure for a `void` endpoint that somebody is WAITING on (rpc or external). */
|
|
63
|
+
export declare const VOID_RPC_CURE: string;
|
|
64
|
+
/**
|
|
65
|
+
* Why the resolver could not map this type, said in the author's vocabulary.
|
|
66
|
+
*
|
|
67
|
+
* The DECISION to refuse is the extractor's and is not second-guessed here — every entry in
|
|
68
|
+
* `model.unmapped` becomes a defect. What this adds is the MESSAGE: an open enum and a mixed scalar
|
|
69
|
+
* union are the two shapes a real repo actually hits (measured: 6 of 98 endpoints, from exactly
|
|
70
|
+
* these two), and "no model representation for this type form" tells their author nothing.
|
|
71
|
+
*/
|
|
72
|
+
export declare function classifyUnmapped(unmapped: UnmappedType): Verdict;
|
|
73
|
+
/**
|
|
74
|
+
* True when this type PUBLISHES an `unknown` value.
|
|
75
|
+
*
|
|
76
|
+
* It looks at the VALUE TYPE wherever it sits — the item of an array, the value of an open map, the
|
|
77
|
+
* field itself — and never at the `Record<>` spelling, because the real cases in the wild are not
|
|
78
|
+
* all Records: `params?: unknown[]` is the same defect written differently.
|
|
79
|
+
*
|
|
80
|
+
* It does NOT descend through a `ref`: a named DTO is its own entry in the model and its own fields
|
|
81
|
+
* are judged there, so descending would report the same field once per type that points at it.
|
|
82
|
+
*/
|
|
83
|
+
export declare function carriesUnknown(ref: TypeRef): boolean;
|
|
84
|
+
/**
|
|
85
|
+
* `void`, `unknown` and `any` all reach the model as the same primitive — the resolver maps the
|
|
86
|
+
* three keywords onto one, because to a document they say the identical thing: nothing.
|
|
87
|
+
*/
|
|
88
|
+
export declare function isVoidLike(ref: TypeRef): boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Every reason this tool could not be served, in the order `McpToolRegistry` asks them at boot —
|
|
91
|
+
* unique name, trigger kind, HTTP auth, MCP auth — and then the renderer's own verdict.
|
|
92
|
+
*
|
|
93
|
+
* The registry's four are restated from the model rather than driven, because they are decorator
|
|
94
|
+
* PRESENCE facts the model already carries verbatim; the SCHEMA question is the one with a real
|
|
95
|
+
* implementation behind it, and that one is driven.
|
|
96
|
+
*/
|
|
97
|
+
export declare function toolFailures(endpoint: DocumentedEndpoint, renderer: McpSchemaRenderer, toolNames: Map<string, string>, model: ApiDocModel): readonly Verdict[];
|
|
98
|
+
/** Every `@ApiPath` class name declared at the top level of a file, in declaration order. */
|
|
99
|
+
export declare function contractNamesIn(source: ts.SourceFile): string[];
|
|
100
|
+
/**
|
|
101
|
+
* Where each method of one contract starts, and which of them carry `@WpMcpTool` without
|
|
102
|
+
* `@Endpoint`.
|
|
103
|
+
*
|
|
104
|
+
* The line numbers exist because the MCP failures come from the RENDERER, whose location is a NAME
|
|
105
|
+
* (`Order.window`) rather than a file offset — it publishes a schema, not a diagnostic. Anchoring
|
|
106
|
+
* every tool failure at its METHOD is also where an author would write the disable: "this tool is
|
|
107
|
+
* deliberately not renderable" is a decision about the method, not about a field three DTOs down.
|
|
108
|
+
*/
|
|
109
|
+
export declare function contractLinesOf(source: ts.SourceFile, contractName: string): ContractLines;
|