@webpieces/nx-webpieces-rules 0.4.807 → 0.4.809

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.
@@ -0,0 +1,308 @@
1
+ "use strict";
2
+ /**
3
+ * The pure VERDICTS the api-contract rules print, split out of `api-doc-rules-scan.ts`, which owns
4
+ * the walk and is at its file-size limit.
5
+ *
6
+ * Nothing here touches the filesystem or the project graph. Each function answers one question about
7
+ * ONE declaration the extractor or the renderer has already ruled on, and turns it into the two
8
+ * halves every webpieces refusal carries — what is wrong, and what to do instead. The DECISION is
9
+ * never made here: `model.unmapped` is the extractor's and a render failure is the renderer's. What
10
+ * this file adds is the WORDING, which is the part a generic "no model representation for this type
11
+ * form" cannot give an author.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.VOID_RPC_CURE = exports.ContractLines = exports.Verdict = exports.RPC_KIND = void 0;
15
+ exports.unknownValueCure = unknownValueCure;
16
+ exports.classifyUnmapped = classifyUnmapped;
17
+ exports.carriesUnknown = carriesUnknown;
18
+ exports.isVoidLike = isVoidLike;
19
+ exports.toolFailures = toolFailures;
20
+ exports.contractNamesIn = contractNamesIn;
21
+ exports.contractLinesOf = contractLinesOf;
22
+ const tslib_1 = require("tslib");
23
+ const ts = tslib_1.__importStar(require("typescript"));
24
+ const api_doc_model_1 = require("@webpieces/api-doc-model");
25
+ /**
26
+ * The one trigger kind an MCP tool may have. A LITERAL, for the same reason `EndpointKind` beside it
27
+ * is one: this package deliberately does not depend on `@webpieces/core-util` at runtime, and the
28
+ * set the literal belongs to is pinned by that type.
29
+ */
30
+ exports.RPC_KIND = 'rpc';
31
+ /** The scalar keywords a MIXED SCALAR union is made of. */
32
+ const SCALAR_KEYWORDS = new Set([
33
+ ts.SyntaxKind.StringKeyword,
34
+ ts.SyntaxKind.NumberKeyword,
35
+ ts.SyntaxKind.BooleanKeyword,
36
+ ]);
37
+ /** ONE reason a declaration is refused, in the two halves every refusal prints. */
38
+ class Verdict {
39
+ what;
40
+ cure;
41
+ constructor(what, cure) {
42
+ this.what = what;
43
+ this.cure = cure;
44
+ }
45
+ }
46
+ exports.Verdict = Verdict;
47
+ /** Method name -> the line its declaration starts on, for one contract class. */
48
+ class ContractLines {
49
+ classLine;
50
+ byMethod;
51
+ toolsWithoutEndpoint;
52
+ constructor(classLine, byMethod,
53
+ /** Methods carrying `@WpMcpTool` that do NOT also carry `@Endpoint`. */
54
+ toolsWithoutEndpoint) {
55
+ this.classLine = classLine;
56
+ this.byMethod = byMethod;
57
+ this.toolsWithoutEndpoint = toolsWithoutEndpoint;
58
+ }
59
+ lineOf(methodName) {
60
+ return this.byMethod.get(methodName) ?? this.classLine;
61
+ }
62
+ }
63
+ exports.ContractLines = ContractLines;
64
+ /**
65
+ * The cure for an `unknown` value, for whichever rule is REPORTING it.
66
+ *
67
+ * It takes the rule name rather than naming one, because this defect is shared: it blocks the
68
+ * OpenAPI document, so `DefectSink` routes it to `api-rules-for-openapi` when that rule is running
69
+ * and to `api-rules-for-mcp` when it is the only one. A hard-coded rule name would then hand an
70
+ * author, on the mcp-only configuration, a `// webpieces-disable api-rules-for-openapi` line that
71
+ * the collector reading the site does not look for — a cure that does not cure, which is the one
72
+ * failure mode a printed cure must not have.
73
+ */
74
+ // webpieces-disable no-function-outside-class -- pure message builder, beside the verdicts that print it
75
+ function unknownValueCure(rule) {
76
+ return ('Name the value type — a DTO, an array of one, a string-literal union, or ' +
77
+ 'Record<string, ThatDto>. If the shape really is unknowable (a transport envelope, ' +
78
+ 'arbitrary SQL rows), write the per-site disable and say so: ' +
79
+ `// webpieces-disable ${rule} -- <why this field is published with no shape>. ` +
80
+ 'An existing no-any-unknown disable does NOT count — that rule asked whether the code is ' +
81
+ 'type-safe, which is a different question from whether a partner is handed a shapeless field.');
82
+ }
83
+ /** The one cure for a `void` RPC. */
84
+ exports.VOID_RPC_CURE = 'Declare a named response DTO and return Promise<That>, even when it has no fields today: an ' +
85
+ 'empty object grows additively forever, and a void response cannot gain a field without ' +
86
+ 'breaking every generated client. Promise<void> stays legal on a cloudtasks or cron endpoint, ' +
87
+ 'where fire-and-forget is the contract.';
88
+ /**
89
+ * Why the resolver could not map this type, said in the author's vocabulary.
90
+ *
91
+ * The DECISION to refuse is the extractor's and is not second-guessed here — every entry in
92
+ * `model.unmapped` becomes a defect. What this adds is the MESSAGE: an open enum and a mixed scalar
93
+ * union are the two shapes a real repo actually hits (measured: 6 of 98 endpoints, from exactly
94
+ * these two), and "no model representation for this type form" tells their author nothing.
95
+ */
96
+ // webpieces-disable no-function-outside-class -- pure classifier over the extractor's own verdict
97
+ function classifyUnmapped(unmapped) {
98
+ const branches = unionBranchesOf(unmapped.typeText);
99
+ if (isOpenEnum(branches)) {
100
+ return new Verdict(`'${unmapped.typeText}' is an OPEN ENUM — literals unioned with the wide type`, 'Use an enum OR a string, not both. TypeScript COLLAPSES this union to the wide type, ' +
101
+ 'so the literals buy no compile-time safety and a document cannot state them as a ' +
102
+ "closed set. Either drop the `| string` (`'a' | 'b'` publishes as a real enum), or " +
103
+ 'drop the literals and list the known values in the JSDoc.');
104
+ }
105
+ if (isMixedScalarUnion(branches)) {
106
+ return new Verdict(`'${unmapped.typeText}' is a MIXED SCALAR union`, 'Give the field ONE type. JSON Schema can spell {"type": ["string","number"]}, but ' +
107
+ 'this framework\'s ApiJsonSchema helpers read a type array as "one real type plus ' +
108
+ 'maybe null" (baseTypeOf returns the first non-null member), so publishing it ' +
109
+ 'would VALIDATE WRONGLY — which is worse than refusing it. Pick the type, or wrap ' +
110
+ 'the alternatives in a discriminated union of named objects.');
111
+ }
112
+ return new Verdict(`'${unmapped.typeText}' has no shape a document can state — ${unmapped.reason}`, 'Give it a type a document can carry: a named DTO, an array of one, a string-literal ' +
113
+ 'union, a Record<string, ThatDto>, or a primitive. A field with no shape publishes as ' +
114
+ '"anything".');
115
+ }
116
+ /** The branches of `typeText` when it parses as a union, else an empty list. */
117
+ // webpieces-disable no-function-outside-class -- pure parser used by classifyUnmapped alone
118
+ function unionBranchesOf(typeText) {
119
+ const parsed = ts.createSourceFile('__unmapped.ts', `type __X = ${typeText};`, ts.ScriptTarget.Latest, true);
120
+ const alias = parsed.statements[0];
121
+ if (alias === undefined || !ts.isTypeAliasDeclaration(alias))
122
+ return [];
123
+ if (!ts.isUnionTypeNode(alias.type))
124
+ return [];
125
+ return alias.type.types.filter((branch) => !isNullishBranch(branch));
126
+ }
127
+ /** `'a' | 'b' | string`, and the numeric equivalent. See #1010 and the decision on #1011. */
128
+ // webpieces-disable no-function-outside-class -- pure predicate beside classifyUnmapped
129
+ function isOpenEnum(branches) {
130
+ const wide = branches.some((branch) => branch.kind === ts.SyntaxKind.StringKeyword ||
131
+ branch.kind === ts.SyntaxKind.NumberKeyword);
132
+ const literals = branches.some((branch) => ts.isLiteralTypeNode(branch));
133
+ return wide && literals;
134
+ }
135
+ /** `string | number`, `string | boolean | null` — two or more DIFFERENT scalar keywords. */
136
+ // webpieces-disable no-function-outside-class -- pure predicate beside classifyUnmapped
137
+ function isMixedScalarUnion(branches) {
138
+ if (branches.length < 2)
139
+ return false;
140
+ const kinds = new Set();
141
+ for (const branch of branches) {
142
+ if (!SCALAR_KEYWORDS.has(branch.kind))
143
+ return false;
144
+ kinds.add(branch.kind);
145
+ }
146
+ return kinds.size >= 2;
147
+ }
148
+ /** `null` / `undefined` branches — nullability, not composition, exactly as the resolver reads it. */
149
+ // webpieces-disable no-function-outside-class -- pure predicate beside classifyUnmapped
150
+ function isNullishBranch(node) {
151
+ if (node.kind === ts.SyntaxKind.UndefinedKeyword || node.kind === ts.SyntaxKind.NullKeyword) {
152
+ return true;
153
+ }
154
+ return ts.isLiteralTypeNode(node) && node.literal.kind === ts.SyntaxKind.NullKeyword;
155
+ }
156
+ /**
157
+ * True when this type PUBLISHES an `unknown` value.
158
+ *
159
+ * It looks at the VALUE TYPE wherever it sits — the item of an array, the value of an open map, the
160
+ * field itself — and never at the `Record<>` spelling, because the real cases in the wild are not
161
+ * all Records: `params?: unknown[]` is the same defect written differently.
162
+ *
163
+ * It does NOT descend through a `ref`: a named DTO is its own entry in the model and its own fields
164
+ * are judged there, so descending would report the same field once per type that points at it.
165
+ */
166
+ // webpieces-disable no-function-outside-class -- pure predicate over one TypeRef
167
+ function carriesUnknown(ref) {
168
+ if (ref.kind === 'primitive')
169
+ return ref.primitive === 'unknown';
170
+ if (ref.kind === 'array')
171
+ return carriesUnknown(ref.items);
172
+ if (ref.kind === 'openMap')
173
+ return carriesUnknown(ref.values);
174
+ return false;
175
+ }
176
+ /**
177
+ * `void`, `unknown` and `any` all reach the model as the same primitive — the resolver maps the
178
+ * three keywords onto one, because to a document they say the identical thing: nothing.
179
+ */
180
+ // webpieces-disable no-function-outside-class -- pure predicate over one TypeRef
181
+ function isVoidLike(ref) {
182
+ return ref.kind === 'primitive' && ref.primitive === 'unknown';
183
+ }
184
+ /**
185
+ * Every reason this tool could not be served, in the order `McpToolRegistry` asks them at boot —
186
+ * unique name, trigger kind, HTTP auth, MCP auth — and then the renderer's own verdict.
187
+ *
188
+ * The registry's four are restated from the model rather than driven, because they are decorator
189
+ * PRESENCE facts the model already carries verbatim; the SCHEMA question is the one with a real
190
+ * implementation behind it, and that one is driven.
191
+ */
192
+ // webpieces-disable no-function-outside-class, max-lines-new-methods -- pure judge over one endpoint, and the four boot checks read as one list
193
+ function toolFailures(endpoint, renderer, toolNames, model) {
194
+ const found = [];
195
+ const toolName = endpoint.mcpTool.name;
196
+ const claimed = toolNames.get(toolName);
197
+ if (claimed === undefined) {
198
+ toolNames.set(toolName, `${model.contractName}.${endpoint.methodName}`);
199
+ }
200
+ else {
201
+ found.push(new Verdict(`tool name '${toolName}' is already declared by ${claimed}`, 'Tool names are the protocol identity and are globally unique — rename one of the ' +
202
+ 'two @WpMcpTool declarations.'));
203
+ }
204
+ if (endpoint.kind !== exports.RPC_KIND) {
205
+ found.push(new Verdict(`an MCP tool is declared on a '${endpoint.kind}' endpoint`, 'Only an RPC endpoint can be a tool — a cloudtasks, cron or external endpoint is ' +
206
+ 'driven by something other than a caller. Make it RPC, or drop the @WpMcpTool.'));
207
+ }
208
+ if (endpoint.auth === undefined) {
209
+ found.push(new Verdict('an MCP tool declares no HTTP auth', 'Put a @WpAuth... decorator on the method. The MCP server refuses to boot without ' +
210
+ 'one, because a tool with no credential is an unauthenticated endpoint an ' +
211
+ 'agent can call.'));
212
+ }
213
+ if (endpoint.mcpAuthText === undefined) {
214
+ found.push(new Verdict('an MCP tool does not declare @WpMcpAuthJwt(...)', 'Add @WpMcpAuthJwt(...) beside the HTTP auth. MCP authorization is rechecked ' +
215
+ 'before the endpoint boundary and is declared separately on purpose.'));
216
+ }
217
+ const rendered = renderFailure(renderer, endpoint);
218
+ if (rendered !== undefined)
219
+ found.push(rendered);
220
+ return found;
221
+ }
222
+ /**
223
+ * The renderer's own refusal for one tool, or undefined when it renders.
224
+ *
225
+ * This is the whole acceptance contract for the MCP half: "passes the rule" and "renders a tool" are
226
+ * the SAME statement, because the rule asks the renderer. It covers the method's missing JSDoc,
227
+ * every reachable DTO field's missing JSDoc, a request or response that is not a named object, a
228
+ * root-level union, a recursive DTO, an `unknown` leaf and a malformed `@mcpHeader` — none of which
229
+ * is restated here.
230
+ *
231
+ * One verdict per tool, because the renderer stops at the first thing it cannot draw. A tool blocked
232
+ * by several undocumented fields is therefore fixed a round at a time, which is the price of having
233
+ * exactly one definition of renderable rather than a second walk that could disagree with it.
234
+ */
235
+ // webpieces-disable no-function-outside-class -- pure judge beside toolFailures
236
+ function renderFailure(renderer, endpoint) {
237
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- the render refusal IS the verdict
238
+ try {
239
+ renderer.tool(endpoint);
240
+ return undefined;
241
+ }
242
+ catch (err) {
243
+ //const error = toError(err);
244
+ if (!(err instanceof api_doc_model_1.McpRenderError))
245
+ throw err;
246
+ return new Verdict(`${err.message} — no MCP schema could be built`, err.cure);
247
+ }
248
+ }
249
+ /** Every `@ApiPath` class name declared at the top level of a file, in declaration order. */
250
+ // webpieces-disable no-function-outside-class -- pure AST reader
251
+ function contractNamesIn(source) {
252
+ const names = [];
253
+ for (const statement of source.statements) {
254
+ if (!ts.isClassDeclaration(statement) || statement.name === undefined)
255
+ continue;
256
+ if (hasDecoratorNamed(statement, 'ApiPath'))
257
+ names.push(statement.name.text);
258
+ }
259
+ return names;
260
+ }
261
+ /**
262
+ * Where each method of one contract starts, and which of them carry `@WpMcpTool` without
263
+ * `@Endpoint`.
264
+ *
265
+ * The line numbers exist because the MCP failures come from the RENDERER, whose location is a NAME
266
+ * (`Order.window`) rather than a file offset — it publishes a schema, not a diagnostic. Anchoring
267
+ * every tool failure at its METHOD is also where an author would write the disable: "this tool is
268
+ * deliberately not renderable" is a decision about the method, not about a field three DTOs down.
269
+ */
270
+ // webpieces-disable no-function-outside-class -- pure AST reader beside contractNamesIn
271
+ function contractLinesOf(source, contractName) {
272
+ for (const statement of source.statements) {
273
+ if (!ts.isClassDeclaration(statement) || statement.name?.text !== contractName)
274
+ continue;
275
+ const byMethod = new Map();
276
+ const orphanTools = [];
277
+ for (const member of statement.members) {
278
+ if (!ts.isMethodDeclaration(member) || !ts.isIdentifier(member.name))
279
+ continue;
280
+ byMethod.set(member.name.text, lineOf(source, member));
281
+ if (hasDecoratorNamed(member, 'WpMcpTool') && !hasDecoratorNamed(member, 'Endpoint')) {
282
+ orphanTools.push(member.name.text);
283
+ }
284
+ }
285
+ return new ContractLines(lineOf(source, statement), byMethod, orphanTools);
286
+ }
287
+ return new ContractLines(1, new Map(), []);
288
+ }
289
+ /** 1-based line of a node's first token, decorators excluded. */
290
+ // webpieces-disable no-function-outside-class -- pure AST reader
291
+ function lineOf(source, node) {
292
+ return source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1;
293
+ }
294
+ /** True when `node` carries `@name(...)`. Matched on the syntax, like every reader in this file. */
295
+ // webpieces-disable no-function-outside-class -- pure AST reader
296
+ function hasDecoratorNamed(node, name) {
297
+ const decorators = ts.canHaveDecorators(node) ? (ts.getDecorators(node) ?? []) : [];
298
+ for (const decorator of decorators) {
299
+ const call = decorator.expression;
300
+ if (ts.isCallExpression(call) &&
301
+ ts.isIdentifier(call.expression) &&
302
+ call.expression.text === name) {
303
+ return true;
304
+ }
305
+ }
306
+ return false;
307
+ }
308
+ //# sourceMappingURL=api-doc-rules-verdicts.js.map
@@ -0,0 +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":";AAAA;;;;;;;;;;GAUG;;;AA8DH,4CASC;AAkBD,4CA2BC;AA6DD,wCAKC;AAOD,gCAEC;AAWD,oCAmDC;AAiCD,0CAOC;AAYD,0CAeC;;AA9TD,uDAAiC;AACjC,4DAOkC;AAGlC;;;;GAIG;AACU,QAAA,QAAQ,GAAiB,KAAK,CAAC;AAE5C,2DAA2D;AAC3D,MAAM,eAAe,GAAG,IAAI,GAAG,CAAgB;IAC3C,EAAE,CAAC,UAAU,CAAC,aAAa;IAC3B,EAAE,CAAC,UAAU,CAAC,aAAa;IAC3B,EAAE,CAAC,UAAU,CAAC,cAAc;CAC/B,CAAC,CAAC;AAEH,mFAAmF;AACnF,MAAa,OAAO;IAEI;IACA;IAFpB,YACoB,IAAY,EACZ,IAAY;QADZ,SAAI,GAAJ,IAAI,CAAQ;QACZ,SAAI,GAAJ,IAAI,CAAQ;IAC7B,CAAC;CACP;AALD,0BAKC;AAGD,iFAAiF;AACjF,MAAa,aAAa;IAEF;IACA;IAEA;IAJpB,YACoB,SAAiB,EACjB,QAAqC;IACrD,wEAAwE;IACxD,oBAAuC;QAHvC,cAAS,GAAT,SAAS,CAAQ;QACjB,aAAQ,GAAR,QAAQ,CAA6B;QAErC,yBAAoB,GAApB,oBAAoB,CAAmB;IACxD,CAAC;IAEJ,MAAM,CAAC,UAAkB;QACrB,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC;IAC3D,CAAC;CACJ;AAXD,sCAWC;AAGD;;;;;;;;;GASG;AACH,yGAAyG;AACzG,SAAgB,gBAAgB,CAAC,IAAY;IACzC,OAAO,CACH,2EAA2E;QAC3E,oFAAoF;QACpF,8DAA8D;QAC9D,wBAAwB,IAAI,mDAAmD;QAC/E,0FAA0F;QAC1F,8FAA8F,CACjG,CAAC;AACN,CAAC;AAED,qCAAqC;AACxB,QAAA,aAAa,GACtB,8FAA8F;IAC9F,yFAAyF;IACzF,+FAA+F;IAC/F,wCAAwC,CAAC;AAE7C;;;;;;;GAOG;AACH,kGAAkG;AAClG,SAAgB,gBAAgB,CAAC,QAAsB;IACnD,MAAM,QAAQ,GAAG,eAAe,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACpD,IAAI,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QACvB,OAAO,IAAI,OAAO,CACd,IAAI,QAAQ,CAAC,QAAQ,yDAAyD,EAC9E,uFAAuF;YACnF,mFAAmF;YACnF,oFAAoF;YACpF,2DAA2D,CAClE,CAAC;IACN,CAAC;IACD,IAAI,kBAAkB,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC/B,OAAO,IAAI,OAAO,CACd,IAAI,QAAQ,CAAC,QAAQ,2BAA2B,EAChD,oFAAoF;YAChF,mFAAmF;YACnF,+EAA+E;YAC/E,mFAAmF;YACnF,6DAA6D,CACpE,CAAC;IACN,CAAC;IACD,OAAO,IAAI,OAAO,CACd,IAAI,QAAQ,CAAC,QAAQ,yCAAyC,QAAQ,CAAC,MAAM,EAAE,EAC/E,sFAAsF;QAClF,uFAAuF;QACvF,aAAa,CACpB,CAAC;AACN,CAAC;AAED,gFAAgF;AAChF,4FAA4F;AAC5F,SAAS,eAAe,CAAC,QAAgB;IACrC,MAAM,MAAM,GAAG,EAAE,CAAC,gBAAgB,CAC9B,eAAe,EACf,cAAc,QAAQ,GAAG,EACzB,EAAE,CAAC,YAAY,CAAC,MAAM,EACtB,IAAI,CACP,CAAC;IACF,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IACnC,IAAI,KAAK,KAAK,SAAS,IAAI,CAAC,EAAE,CAAC,sBAAsB,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACxE,IAAI,CAAC,EAAE,CAAC,eAAe,CAAC,KAAK,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IAC/C,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,MAAmB,EAAE,EAAE,CAAC,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC;AACtF,CAAC;AAED,6FAA6F;AAC7F,wFAAwF;AACxF,SAAS,UAAU,CAAC,QAAgC;IAChD,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CACtB,CAAC,MAAmB,EAAE,EAAE,CACpB,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC,UAAU,CAAC,aAAa;QAC3C,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC,UAAU,CAAC,aAAa,CAClD,CAAC;IACF,MAAM,QAAQ,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,MAAmB,EAAE,EAAE,CAAC,EAAE,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAC,CAAC;IACtF,OAAO,IAAI,IAAI,QAAQ,CAAC;AAC5B,CAAC;AAED,4FAA4F;AAC5F,wFAAwF;AACxF,SAAS,kBAAkB,CAAC,QAAgC;IACxD,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IACtC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAiB,CAAC;IACvC,KAAK,MAAM,MAAM,IAAI,QAAQ,EAAE,CAAC;QAC5B,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;YAAE,OAAO,KAAK,CAAC;QACpD,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC3B,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC;AAC3B,CAAC;AAED,sGAAsG;AACtG,wFAAwF;AACxF,SAAS,eAAe,CAAC,IAAiB;IACtC,IAAI,IAAI,CAAC,IAAI,KAAK,EAAE,CAAC,UAAU,CAAC,gBAAgB,IAAI,IAAI,CAAC,IAAI,KAAK,EAAE,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC;QAC1F,OAAO,IAAI,CAAC;IAChB,CAAC;IACD,OAAO,EAAE,CAAC,iBAAiB,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC;AACzF,CAAC;AAED;;;;;;;;;GASG;AACH,iFAAiF;AACjF,SAAgB,cAAc,CAAC,GAAY;IACvC,IAAI,GAAG,CAAC,IAAI,KAAK,WAAW;QAAE,OAAO,GAAG,CAAC,SAAS,KAAK,SAAS,CAAC;IACjE,IAAI,GAAG,CAAC,IAAI,KAAK,OAAO;QAAE,OAAO,cAAc,CAAC,GAAG,CAAC,KAAM,CAAC,CAAC;IAC5D,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS;QAAE,OAAO,cAAc,CAAC,GAAG,CAAC,MAAO,CAAC,CAAC;IAC/D,OAAO,KAAK,CAAC;AACjB,CAAC;AAED;;;GAGG;AACH,iFAAiF;AACjF,SAAgB,UAAU,CAAC,GAAY;IACnC,OAAO,GAAG,CAAC,IAAI,KAAK,WAAW,IAAI,GAAG,CAAC,SAAS,KAAK,SAAS,CAAC;AACnE,CAAC;AAED;;;;;;;GAOG;AACH,gJAAgJ;AAChJ,SAAgB,YAAY,CACxB,QAA4B,EAC5B,QAA2B,EAC3B,SAA8B,EAC9B,KAAkB;IAElB,MAAM,KAAK,GAAc,EAAE,CAAC;IAC5B,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAQ,CAAC,IAAI,CAAC;IACxC,MAAM,OAAO,GAAG,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACxC,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QACxB,SAAS,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,KAAK,CAAC,YAAY,IAAI,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC;IAC5E,CAAC;SAAM,CAAC;QACJ,KAAK,CAAC,IAAI,CACN,IAAI,OAAO,CACP,cAAc,QAAQ,4BAA4B,OAAO,EAAE,EAC3D,mFAAmF;YAC/E,8BAA8B,CACrC,CACJ,CAAC;IACN,CAAC;IACD,IAAI,QAAQ,CAAC,IAAI,KAAK,gBAAQ,EAAE,CAAC;QAC7B,KAAK,CAAC,IAAI,CACN,IAAI,OAAO,CACP,iCAAiC,QAAQ,CAAC,IAAI,YAAY,EAC1D,kFAAkF;YAC9E,+EAA+E,CACtF,CACJ,CAAC;IACN,CAAC;IACD,IAAI,QAAQ,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC9B,KAAK,CAAC,IAAI,CACN,IAAI,OAAO,CACP,mCAAmC,EACnC,mFAAmF;YAC/E,2EAA2E;YAC3E,iBAAiB,CACxB,CACJ,CAAC;IACN,CAAC;IACD,IAAI,QAAQ,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;QACrC,KAAK,CAAC,IAAI,CACN,IAAI,OAAO,CACP,iDAAiD,EACjD,8EAA8E;YAC1E,qEAAqE,CAC5E,CACJ,CAAC;IACN,CAAC;IACD,MAAM,QAAQ,GAAG,aAAa,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IACnD,IAAI,QAAQ,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACjD,OAAO,KAAK,CAAC;AACjB,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,gFAAgF;AAChF,SAAS,aAAa,CAClB,QAA2B,EAC3B,QAA4B;IAE5B,mGAAmG;IACnG,IAAI,CAAC;QACD,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACxB,OAAO,SAAS,CAAC;IACrB,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACpB,6BAA6B;QAC7B,IAAI,CAAC,CAAC,GAAG,YAAY,8BAAc,CAAC;YAAE,MAAM,GAAG,CAAC;QAChD,OAAO,IAAI,OAAO,CAAC,GAAG,GAAG,CAAC,OAAO,iCAAiC,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC;IAClF,CAAC;AACL,CAAC;AAED,6FAA6F;AAC7F,iEAAiE;AACjE,SAAgB,eAAe,CAAC,MAAqB;IACjD,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,SAAS,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;QACxC,IAAI,CAAC,EAAE,CAAC,kBAAkB,CAAC,SAAS,CAAC,IAAI,SAAS,CAAC,IAAI,KAAK,SAAS;YAAE,SAAS;QAChF,IAAI,iBAAiB,CAAC,SAAS,EAAE,SAAS,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjF,CAAC;IACD,OAAO,KAAK,CAAC;AACjB,CAAC;AAED;;;;;;;;GAQG;AACH,wFAAwF;AACxF,SAAgB,eAAe,CAAC,MAAqB,EAAE,YAAoB;IACvE,KAAK,MAAM,SAAS,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;QACxC,IAAI,CAAC,EAAE,CAAC,kBAAkB,CAAC,SAAS,CAAC,IAAI,SAAS,CAAC,IAAI,EAAE,IAAI,KAAK,YAAY;YAAE,SAAS;QACzF,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAC;QAC3C,MAAM,WAAW,GAAa,EAAE,CAAC;QACjC,KAAK,MAAM,MAAM,IAAI,SAAS,CAAC,OAAO,EAAE,CAAC;YACrC,IAAI,CAAC,EAAE,CAAC,mBAAmB,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC;gBAAE,SAAS;YAC/E,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;YACvD,IAAI,iBAAiB,CAAC,MAAM,EAAE,WAAW,CAAC,IAAI,CAAC,iBAAiB,CAAC,MAAM,EAAE,UAAU,CAAC,EAAE,CAAC;gBACnF,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACvC,CAAC;QACL,CAAC;QACD,OAAO,IAAI,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC;IAC/E,CAAC;IACD,OAAO,IAAI,aAAa,CAAC,CAAC,EAAE,IAAI,GAAG,EAAkB,EAAE,EAAE,CAAC,CAAC;AAC/D,CAAC;AAED,iEAAiE;AACjE,iEAAiE;AACjE,SAAS,MAAM,CAAC,MAAqB,EAAE,IAAa;IAChD,OAAO,MAAM,CAAC,6BAA6B,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC;AAChF,CAAC;AAED,oGAAoG;AACpG,iEAAiE;AACjE,SAAS,iBAAiB,CAAC,IAAa,EAAE,IAAY;IAClD,MAAM,UAAU,GAAG,EAAE,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACpF,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACjC,MAAM,IAAI,GAAG,SAAS,CAAC,UAAU,CAAC;QAClC,IACI,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC;YACzB,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC;YAChC,IAAI,CAAC,UAAU,CAAC,IAAI,KAAK,IAAI,EAC/B,CAAC;YACC,OAAO,IAAI,CAAC;QAChB,CAAC;IACL,CAAC;IACD,OAAO,KAAK,CAAC;AACjB,CAAC","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"]}
@@ -0,0 +1,149 @@
1
+ /**
2
+ * The DATA and the SWITCHES of `api-rules-for-openapi` and `api-rules-for-mcp` (#1011).
3
+ *
4
+ * The scan itself is in `api-doc-rules-scan.ts`; this file holds what a caller reads back and the
5
+ * per-rule config, so a unit test can construct either without touching a config file and so the
6
+ * config is read exactly once per executor run.
7
+ */
8
+ /** As written in a disable comment and as a config key. */
9
+ export declare const OPENAPI_RULE: "api-rules-for-openapi";
10
+ export declare const MCP_RULE: "api-rules-for-mcp";
11
+ /** The `@ApiType` value that means "a partner reads this document". */
12
+ export declare const EXTERNAL_CUSTOMER_API_TYPE = "external-customer";
13
+ /**
14
+ * ONE defect on ONE contract.
15
+ *
16
+ * `exposure` is the contract's `@ApiType` list, and it is carried rather than derived because the
17
+ * SAME defect is louder on a contract that reaches `external-customer`: an unshaped field on a
18
+ * partner document costs a partner something, where the same field on a service-to-service contract
19
+ * costs a colleague a question. The refusal sorts and labels on it.
20
+ */
21
+ export declare class ApiContractDefect {
22
+ /** The `@ApiPath` contract class. */
23
+ readonly api: string;
24
+ /** The `@Endpoint` method, or `''` for a contract-level or DTO-level defect. */
25
+ readonly method: string;
26
+ /** What is wrong, in one line, naming the declaration. */
27
+ readonly what: string;
28
+ /** `path/to/File.ts:LINE`, workspace-relative. */
29
+ readonly at: string;
30
+ /** What to do instead, in one sentence. */
31
+ readonly cure: string;
32
+ /** The contract's `@ApiType` list — `svc-to-svc` when it declares none. */
33
+ readonly exposure: readonly string[];
34
+ constructor(
35
+ /** The `@ApiPath` contract class. */
36
+ api: string,
37
+ /** The `@Endpoint` method, or `''` for a contract-level or DTO-level defect. */
38
+ method: string,
39
+ /** What is wrong, in one line, naming the declaration. */
40
+ what: string,
41
+ /** `path/to/File.ts:LINE`, workspace-relative. */
42
+ at: string,
43
+ /** What to do instead, in one sentence. */
44
+ cure: string,
45
+ /** The contract's `@ApiType` list — `svc-to-svc` when it declares none. */
46
+ exposure: readonly string[]);
47
+ /** True when this contract feeds the PARTNER-facing document. */
48
+ isExternal(): boolean;
49
+ /** `Api.method` or `Api` — what a reader opens. */
50
+ where(): string;
51
+ }
52
+ /** What ONE rule found: real defects, and disables of it that gave no reason. */
53
+ export declare class ApiRuleFindings {
54
+ readonly violations: readonly ApiContractDefect[];
55
+ /**
56
+ * Sites that named this rule in a disable and wrote no reason. A reasonless disable is
57
+ * itself a violation: the point of the per-site hatch is the ARGUMENT, which is the only
58
+ * part the next reader can weigh.
59
+ */
60
+ readonly reasonlessDisables: readonly ApiContractDefect[];
61
+ constructor(violations: readonly ApiContractDefect[],
62
+ /**
63
+ * Sites that named this rule in a disable and wrote no reason. A reasonless disable is
64
+ * itself a violation: the point of the per-site hatch is the ARGUMENT, which is the only
65
+ * part the next reader can weigh.
66
+ */
67
+ reasonlessDisables: readonly ApiContractDefect[]);
68
+ isEmpty(): boolean;
69
+ }
70
+ /**
71
+ * ONE endpoint declared PERMANENTLY outside MCP by `@InvalidEndpointForMcp('<reason>')` (#1014).
72
+ *
73
+ * Not a violation and not a suppression — a DECLARATION, which is why it is carried beside the
74
+ * findings rather than in them. `api-rules-for-mcp` restates the whole list, with reasons, on every
75
+ * run: an exclusion announced once at the moment somebody added it is an exclusion nobody will read
76
+ * again, and a build-time warning at add time would be read exactly as little.
77
+ */
78
+ export declare class McpExclusion {
79
+ readonly api: string;
80
+ readonly method: string;
81
+ /** The decorator's reason, verbatim. Empty only when the argument could not be folded. */
82
+ readonly reason: string;
83
+ /** `path/to/File.ts:LINE`, workspace-relative. */
84
+ readonly at: string;
85
+ constructor(api: string, method: string,
86
+ /** The decorator's reason, verbatim. Empty only when the argument could not be folded. */
87
+ reason: string,
88
+ /** `path/to/File.ts:LINE`, workspace-relative. */
89
+ at: string);
90
+ }
91
+ /** Both rules' findings from ONE scan — one program build, two verdicts. */
92
+ export declare class ApiDocRulesFindings {
93
+ readonly openApi: ApiRuleFindings;
94
+ readonly mcp: ApiRuleFindings;
95
+ /** Every `@InvalidEndpointForMcp` endpoint the MCP rule looked at, in declaration order. */
96
+ readonly mcpExclusions: readonly McpExclusion[];
97
+ constructor(openApi: ApiRuleFindings, mcp: ApiRuleFindings,
98
+ /** Every `@InvalidEndpointForMcp` endpoint the MCP rule looked at, in declaration order. */
99
+ mcpExclusions?: readonly McpExclusion[]);
100
+ static empty(): ApiDocRulesFindings;
101
+ }
102
+ /**
103
+ * ONE rule's switches, resolved from webpieces.config.json once per scan.
104
+ *
105
+ * The DEFAULT is OFF, and unlike an absent entry elsewhere that is deliberate: `defaultRules` in
106
+ * `@webpieces/rules-config` carries `mode: 'OFF'` for both, and the argument for it is written
107
+ * there rather than here so there is one place to read it.
108
+ */
109
+ export declare class ApiDocRule {
110
+ readonly name: string;
111
+ readonly enabled: boolean;
112
+ /** Project roots this rule does not apply to — `allowedPaths` in the config. */
113
+ readonly allowedPaths: readonly string[];
114
+ constructor(name: string, enabled: boolean,
115
+ /** Project roots this rule does not apply to — `allowedPaths` in the config. */
116
+ allowedPaths: readonly string[]);
117
+ /** ARMED, everywhere — what a unit test constructs, and what an opted-in repo resolves to. */
118
+ static armed(name: string): ApiDocRule;
119
+ static off(name: string): ApiDocRule;
120
+ /** `mode: OFF` and the time-box/branch hatches come from RuleGate, so there is ONE reading. */
121
+ static fromConfig(workspaceRoot: string, name: string): ApiDocRule;
122
+ }
123
+ /** ONE `webpieces-disable` naming a rule, and whether it gave a reason. */
124
+ export declare class DisableComment {
125
+ readonly hasReason: boolean;
126
+ constructor(hasReason: boolean);
127
+ /**
128
+ * The disable naming `rule` on the declaration at `line` (1-based) of `file`, or undefined.
129
+ *
130
+ * It walks UPWARD over the declaration's own leading trivia — blank lines, `//` comments, a
131
+ * JSDoc block, and the decorators between them — because that is where an author writes one, and
132
+ * it stops at the first line that is none of those, so a directive belonging to the PREVIOUS
133
+ * declaration can never be read as covering this one.
134
+ *
135
+ * Only a directive NAMING this rule counts. An existing `// webpieces-disable no-any-unknown`
136
+ * therefore does not silence these rules, which is the point: that rule answers "is this
137
+ * type-safe?" and these answer "is this field PUBLISHED with no shape?" — different questions
138
+ * with different right answers, so the second one wants its own, separately argued line.
139
+ *
140
+ * A file that cannot be read yields "no disable": a defect is still a defect, and inventing a
141
+ * suppression out of an I/O failure is the one wrong answer.
142
+ */
143
+ static readAt(file: string, line: number, rule: string): DisableComment | undefined;
144
+ /** Leading trivia a disable comment is allowed to sit above: blanks, comments and decorators. */
145
+ private static isTrivia;
146
+ private static match;
147
+ /** The file's lines, or undefined when it cannot be read. */
148
+ private static linesOf;
149
+ }