@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.
@@ -0,0 +1,298 @@
1
+ "use strict";
2
+ /**
3
+ * The DATA and the SWITCHES of `api-rules-for-openapi` and `api-rules-for-mcp` (#1011).
4
+ *
5
+ * The scan itself is in `api-doc-rules-scan.ts`; this file holds what a caller reads back and the
6
+ * per-rule config, so a unit test can construct either without touching a config file and so the
7
+ * config is read exactly once per executor run.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.DisableComment = exports.ApiDocRule = exports.AFFECTED_PROJECT_MODE = exports.ApiDocRulesFindings = exports.McpExclusion = exports.ApiRuleFindings = exports.ApiContractDefect = exports.EXTERNAL_CUSTOMER_API_TYPE = exports.MCP_RULE = exports.OPENAPI_RULE = void 0;
11
+ const tslib_1 = require("tslib");
12
+ const fs = tslib_1.__importStar(require("fs"));
13
+ const rules_config_1 = require("@webpieces/rules-config");
14
+ const rule_gate_1 = require("../rule-gate");
15
+ /** As written in a disable comment and as a config key. */
16
+ exports.OPENAPI_RULE = rules_config_1.RULE_NAMES.API_RULES_FOR_OPENAPI;
17
+ exports.MCP_RULE = rules_config_1.RULE_NAMES.API_RULES_FOR_MCP;
18
+ /** The `@ApiType` value that means "a partner reads this document". */
19
+ exports.EXTERNAL_CUSTOMER_API_TYPE = 'external-customer';
20
+ /**
21
+ * `// webpieces-disable <rule>[, <rule2>] -- <reason>`, with the reason CAPTURED so a reasonless
22
+ * disable can be told apart from an absent one. Identical to the spelling `root-union-scan` reads.
23
+ */
24
+ const DISABLE_RE = new RegExp(`//\\s*${rules_config_1.WEBPIECES_DISABLE}\\s+([\\w-]+(?:\\s*,\\s*[\\w-]+)*)(?:\\s*--\\s*(.*))?$`);
25
+ /** How far above a declaration a disable comment may sit before it is somebody else's comment. */
26
+ const DISABLE_LOOKBACK_LINES = 40;
27
+ /**
28
+ * ONE defect on ONE contract.
29
+ *
30
+ * `exposure` is the contract's `@ApiType` list, and it is carried rather than derived because the
31
+ * SAME defect is louder on a contract that reaches `external-customer`: an unshaped field on a
32
+ * partner document costs a partner something, where the same field on a service-to-service contract
33
+ * costs a colleague a question. The refusal sorts and labels on it.
34
+ */
35
+ class ApiContractDefect {
36
+ api;
37
+ method;
38
+ what;
39
+ at;
40
+ cure;
41
+ exposure;
42
+ constructor(
43
+ /** The `@ApiPath` contract class. */
44
+ api,
45
+ /** The `@Endpoint` method, or `''` for a contract-level or DTO-level defect. */
46
+ method,
47
+ /** What is wrong, in one line, naming the declaration. */
48
+ what,
49
+ /** `path/to/File.ts:LINE`, workspace-relative. */
50
+ at,
51
+ /** What to do instead, in one sentence. */
52
+ cure,
53
+ /** The contract's `@ApiType` list — `svc-to-svc` when it declares none. */
54
+ exposure) {
55
+ this.api = api;
56
+ this.method = method;
57
+ this.what = what;
58
+ this.at = at;
59
+ this.cure = cure;
60
+ this.exposure = exposure;
61
+ }
62
+ /** True when this contract feeds the PARTNER-facing document. */
63
+ isExternal() {
64
+ return this.exposure.includes(exports.EXTERNAL_CUSTOMER_API_TYPE);
65
+ }
66
+ /** `Api.method` or `Api` — what a reader opens. */
67
+ where() {
68
+ return this.method === '' ? this.api : `${this.api}.${this.method}`;
69
+ }
70
+ }
71
+ exports.ApiContractDefect = ApiContractDefect;
72
+ /** What ONE rule found: real defects, and disables of it that gave no reason. */
73
+ class ApiRuleFindings {
74
+ violations;
75
+ reasonlessDisables;
76
+ constructor(violations,
77
+ /**
78
+ * Sites that named this rule in a disable and wrote no reason. A reasonless disable is
79
+ * itself a violation: the point of the per-site hatch is the ARGUMENT, which is the only
80
+ * part the next reader can weigh.
81
+ */
82
+ reasonlessDisables) {
83
+ this.violations = violations;
84
+ this.reasonlessDisables = reasonlessDisables;
85
+ }
86
+ isEmpty() {
87
+ return this.violations.length === 0 && this.reasonlessDisables.length === 0;
88
+ }
89
+ }
90
+ exports.ApiRuleFindings = ApiRuleFindings;
91
+ /**
92
+ * ONE endpoint declared PERMANENTLY outside MCP by `@InvalidEndpointForMcp('<reason>')` (#1014).
93
+ *
94
+ * Not a violation and not a suppression — a DECLARATION, which is why it is carried beside the
95
+ * findings rather than in them. `api-rules-for-mcp` restates the whole list, with reasons, on every
96
+ * run: an exclusion announced once at the moment somebody added it is an exclusion nobody will read
97
+ * again, and a build-time warning at add time would be read exactly as little.
98
+ */
99
+ class McpExclusion {
100
+ api;
101
+ method;
102
+ reason;
103
+ at;
104
+ constructor(api, method,
105
+ /** The decorator's reason, verbatim. Empty only when the argument could not be folded. */
106
+ reason,
107
+ /** `path/to/File.ts:LINE`, workspace-relative. */
108
+ at) {
109
+ this.api = api;
110
+ this.method = method;
111
+ this.reason = reason;
112
+ this.at = at;
113
+ }
114
+ }
115
+ exports.McpExclusion = McpExclusion;
116
+ /** Both rules' findings from ONE scan — one program build, two verdicts. */
117
+ class ApiDocRulesFindings {
118
+ openApi;
119
+ mcp;
120
+ mcpExclusions;
121
+ constructor(openApi, mcp,
122
+ /** Every `@InvalidEndpointForMcp` endpoint the MCP rule looked at, in declaration order. */
123
+ mcpExclusions = []) {
124
+ this.openApi = openApi;
125
+ this.mcp = mcp;
126
+ this.mcpExclusions = mcpExclusions;
127
+ }
128
+ // webpieces-disable no-function-outside-class -- static factory of this class
129
+ static empty() {
130
+ return new ApiDocRulesFindings(new ApiRuleFindings([], []), new ApiRuleFindings([], []), []);
131
+ }
132
+ }
133
+ exports.ApiDocRulesFindings = ApiDocRulesFindings;
134
+ /** The mode value that narrows the scan to the projects the diff touched. */
135
+ exports.AFFECTED_PROJECT_MODE = 'AFFECTED_PROJECT';
136
+ /**
137
+ * ONE rule's switches, resolved from webpieces.config.json once per scan.
138
+ *
139
+ * There is NO default (#1017). Both rules are schema'd, so `webpieces.config.json` must carry an
140
+ * entry for each or the config FAILS TO LOAD naming them — a consumer states `OFF`,
141
+ * `AFFECTED_PROJECT` or `RUN_EVERY_TIME` out loud. The `off()` a bare constructor or a unit test
142
+ * gets is not a default; it is what a caller that configured nothing asked for.
143
+ */
144
+ class ApiDocRule {
145
+ name;
146
+ enabled;
147
+ allowedPaths;
148
+ changedPaths;
149
+ constructor(name, enabled,
150
+ /** Project roots this rule does not apply to — `allowedPaths` in the config. */
151
+ allowedPaths,
152
+ /**
153
+ * Workspace-relative paths the diff touched, under `AFFECTED_PROJECT`; `null` under
154
+ * `RUN_EVERY_TIME`, which means every project is in scope.
155
+ *
156
+ * `null` is also what an UNCOMPUTABLE diff resolves to — no merge-base, a shallow clone, no
157
+ * repository at all. A diff that could not be read is not evidence that nothing changed, and
158
+ * the only safe reading of "I do not know what changed" is "look at all of it".
159
+ */
160
+ changedPaths = null) {
161
+ this.name = name;
162
+ this.enabled = enabled;
163
+ this.allowedPaths = allowedPaths;
164
+ this.changedPaths = changedPaths;
165
+ }
166
+ /** ARMED over every project — what a unit test constructs, and what RUN_EVERY_TIME resolves to. */
167
+ // webpieces-disable no-function-outside-class -- static factory of this class
168
+ static armed(name) {
169
+ return new ApiDocRule(name, true, [], null);
170
+ }
171
+ /** ARMED over the projects owning one of `changedPaths` — what AFFECTED_PROJECT resolves to. */
172
+ // webpieces-disable no-function-outside-class -- static factory of this class
173
+ static affected(name, changedPaths) {
174
+ return new ApiDocRule(name, true, [], changedPaths);
175
+ }
176
+ // webpieces-disable no-function-outside-class -- static factory of this class
177
+ static off(name) {
178
+ return new ApiDocRule(name, false, [], null);
179
+ }
180
+ /**
181
+ * True when this rule scans the contracts of the project rooted at `root`.
182
+ *
183
+ * ONE place answers it, for both rules, so `allowedPaths` and the affected-project narrowing can
184
+ * never be applied by one caller and skipped by another.
185
+ */
186
+ coversProject(root) {
187
+ if (!this.enabled)
188
+ return false;
189
+ if ((0, rules_config_1.matchesAnyGlob)(root, this.allowedPaths))
190
+ return false;
191
+ if (this.changedPaths === null)
192
+ return true;
193
+ const prefix = `${root}/`;
194
+ return this.changedPaths.some((each) => each.startsWith(prefix));
195
+ }
196
+ /** `mode: OFF` and the time-box/branch hatches come from RuleGate, so there is ONE reading. */
197
+ // webpieces-disable no-function-outside-class -- static factory of this class
198
+ static fromConfig(workspaceRoot, name) {
199
+ if (new rule_gate_1.RuleGate().isDisabled(workspaceRoot, name, true)) {
200
+ return ApiDocRule.off(name);
201
+ }
202
+ const rule = (0, rules_config_1.loadAndValidate)(workspaceRoot).resolved.rules.get(name);
203
+ const allowed = rule?.options['allowedPaths'];
204
+ const allowedPaths = Array.isArray(allowed) ? allowed : [];
205
+ const affected = rule?.options['mode'] === exports.AFFECTED_PROJECT_MODE
206
+ ? ApiDocRule.changedPathsOf(workspaceRoot)
207
+ : null;
208
+ return new ApiDocRule(name, true, allowedPaths, affected);
209
+ }
210
+ /**
211
+ * Every path the diff touched, against the base nx itself uses (`NX_BASE`, else the merge-base
212
+ * with origin/main). NOT ts-only and INCLUDING deletions: a deleted DTO changes what a project's
213
+ * contracts can express exactly as an added one does, and a `project.json` edit can change which
214
+ * project owns a contract at all.
215
+ */
216
+ // webpieces-disable no-function-outside-class -- private static reader of this class
217
+ static changedPathsOf(workspaceRoot) {
218
+ const scope = new rules_config_1.DiffScope();
219
+ const range = scope.resolveBase(workspaceRoot);
220
+ if (range.base === undefined)
221
+ return null;
222
+ const opts = new rules_config_1.ChangedFilesOptions();
223
+ opts.tsOnly = false;
224
+ opts.includeDeletions = true;
225
+ return scope.getChangedFiles(workspaceRoot, range.base, range.head, opts);
226
+ }
227
+ }
228
+ exports.ApiDocRule = ApiDocRule;
229
+ /** ONE `webpieces-disable` naming a rule, and whether it gave a reason. */
230
+ class DisableComment {
231
+ hasReason;
232
+ constructor(hasReason) {
233
+ this.hasReason = hasReason;
234
+ }
235
+ /**
236
+ * The disable naming `rule` on the declaration at `line` (1-based) of `file`, or undefined.
237
+ *
238
+ * It walks UPWARD over the declaration's own leading trivia — blank lines, `//` comments, a
239
+ * JSDoc block, and the decorators between them — because that is where an author writes one, and
240
+ * it stops at the first line that is none of those, so a directive belonging to the PREVIOUS
241
+ * declaration can never be read as covering this one.
242
+ *
243
+ * Only a directive NAMING this rule counts. An existing `// webpieces-disable no-any-unknown`
244
+ * therefore does not silence these rules, which is the point: that rule answers "is this
245
+ * type-safe?" and these answer "is this field PUBLISHED with no shape?" — different questions
246
+ * with different right answers, so the second one wants its own, separately argued line.
247
+ *
248
+ * A file that cannot be read yields "no disable": a defect is still a defect, and inventing a
249
+ * suppression out of an I/O failure is the one wrong answer.
250
+ */
251
+ // webpieces-disable no-function-outside-class -- static factory of this class
252
+ static readAt(file, line, rule) {
253
+ const lines = DisableComment.linesOf(file);
254
+ if (lines === undefined)
255
+ return undefined;
256
+ const start = Math.max(0, line - 1);
257
+ const onDeclaration = DisableComment.match(lines[start] ?? '', rule);
258
+ if (onDeclaration !== undefined)
259
+ return onDeclaration;
260
+ for (let i = start - 1; i >= 0 && i >= start - DISABLE_LOOKBACK_LINES; i--) {
261
+ const text = (lines[i] ?? '').trim();
262
+ const above = DisableComment.match(text, rule);
263
+ if (above !== undefined)
264
+ return above;
265
+ if (!DisableComment.isTrivia(text))
266
+ return undefined;
267
+ }
268
+ return undefined;
269
+ }
270
+ /** Leading trivia a disable comment is allowed to sit above: blanks, comments and decorators. */
271
+ // webpieces-disable no-function-outside-class -- private static predicate of this class
272
+ static isTrivia(text) {
273
+ return (text === '' ||
274
+ text.startsWith('//') ||
275
+ text.startsWith('*') ||
276
+ text.startsWith('/*') ||
277
+ text.startsWith('@'));
278
+ }
279
+ // webpieces-disable no-function-outside-class -- private static reader of this class
280
+ static match(text, rule) {
281
+ const found = text.trim().match(DISABLE_RE);
282
+ if (found === null)
283
+ return undefined;
284
+ const named = found[1].split(',').map((each) => each.trim());
285
+ if (!named.includes(rule))
286
+ return undefined;
287
+ return new DisableComment((found[2] ?? '').trim() !== '');
288
+ }
289
+ /** The file's lines, or undefined when it cannot be read. */
290
+ // webpieces-disable no-function-outside-class -- private static reader of this class
291
+ static linesOf(file) {
292
+ if (!fs.existsSync(file))
293
+ return undefined;
294
+ return fs.readFileSync(file, 'utf8').split('\n');
295
+ }
296
+ }
297
+ exports.DisableComment = DisableComment;
298
+ //# sourceMappingURL=api-doc-rules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-doc-rules.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-doc-rules.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The DATA and the SWITCHES of `api-rules-for-openapi` and `api-rules-for-mcp` (#1011).\n *\n * The scan itself is in `api-doc-rules-scan.ts`; this file holds what a caller reads back and the\n * per-rule config, so a unit test can construct either without touching a config file and so the\n * config is read exactly once per executor run.\n */\n\nimport * as fs from 'fs';\nimport {\n ChangedFilesOptions,\n DiffScope,\n loadAndValidate,\n matchesAnyGlob,\n RULE_NAMES,\n WEBPIECES_DISABLE,\n} from '@webpieces/rules-config';\nimport { RuleGate } from '../rule-gate';\n\n/** As written in a disable comment and as a config key. */\nexport const OPENAPI_RULE = RULE_NAMES.API_RULES_FOR_OPENAPI;\nexport const MCP_RULE = RULE_NAMES.API_RULES_FOR_MCP;\n\n/** The `@ApiType` value that means \"a partner reads this document\". */\nexport const EXTERNAL_CUSTOMER_API_TYPE = 'external-customer';\n\n/**\n * `// webpieces-disable <rule>[, <rule2>] -- <reason>`, with the reason CAPTURED so a reasonless\n * disable can be told apart from an absent one. Identical to the spelling `root-union-scan` reads.\n */\nconst DISABLE_RE = new RegExp(\n `//\\\\s*${WEBPIECES_DISABLE}\\\\s+([\\\\w-]+(?:\\\\s*,\\\\s*[\\\\w-]+)*)(?:\\\\s*--\\\\s*(.*))?$`,\n);\n\n/** How far above a declaration a disable comment may sit before it is somebody else's comment. */\nconst DISABLE_LOOKBACK_LINES = 40;\n\n/**\n * ONE defect on ONE contract.\n *\n * `exposure` is the contract's `@ApiType` list, and it is carried rather than derived because the\n * SAME defect is louder on a contract that reaches `external-customer`: an unshaped field on a\n * partner document costs a partner something, where the same field on a service-to-service contract\n * costs a colleague a question. The refusal sorts and labels on it.\n */\nexport class ApiContractDefect {\n constructor(\n /** The `@ApiPath` contract class. */\n public readonly api: string,\n /** The `@Endpoint` method, or `''` for a contract-level or DTO-level defect. */\n public readonly method: string,\n /** What is wrong, in one line, naming the declaration. */\n public readonly what: string,\n /** `path/to/File.ts:LINE`, workspace-relative. */\n public readonly at: string,\n /** What to do instead, in one sentence. */\n public readonly cure: string,\n /** The contract's `@ApiType` list — `svc-to-svc` when it declares none. */\n public readonly exposure: readonly string[],\n ) {}\n\n /** True when this contract feeds the PARTNER-facing document. */\n isExternal(): boolean {\n return this.exposure.includes(EXTERNAL_CUSTOMER_API_TYPE);\n }\n\n /** `Api.method` or `Api` — what a reader opens. */\n where(): string {\n return this.method === '' ? this.api : `${this.api}.${this.method}`;\n }\n}\n\n/** What ONE rule found: real defects, and disables of it that gave no reason. */\nexport class ApiRuleFindings {\n constructor(\n public readonly violations: readonly ApiContractDefect[],\n /**\n * Sites that named this rule in a disable and wrote no reason. A reasonless disable is\n * itself a violation: the point of the per-site hatch is the ARGUMENT, which is the only\n * part the next reader can weigh.\n */\n public readonly reasonlessDisables: readonly ApiContractDefect[],\n ) {}\n\n isEmpty(): boolean {\n return this.violations.length === 0 && this.reasonlessDisables.length === 0;\n }\n}\n\n/**\n * ONE endpoint declared PERMANENTLY outside MCP by `@InvalidEndpointForMcp('<reason>')` (#1014).\n *\n * Not a violation and not a suppression — a DECLARATION, which is why it is carried beside the\n * findings rather than in them. `api-rules-for-mcp` restates the whole list, with reasons, on every\n * run: an exclusion announced once at the moment somebody added it is an exclusion nobody will read\n * again, and a build-time warning at add time would be read exactly as little.\n */\nexport class McpExclusion {\n constructor(\n public readonly api: string,\n public readonly method: string,\n /** The decorator's reason, verbatim. Empty only when the argument could not be folded. */\n public readonly reason: string,\n /** `path/to/File.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/** Both rules' findings from ONE scan — one program build, two verdicts. */\nexport class ApiDocRulesFindings {\n constructor(\n public readonly openApi: ApiRuleFindings,\n public readonly mcp: ApiRuleFindings,\n /** Every `@InvalidEndpointForMcp` endpoint the MCP rule looked at, in declaration order. */\n public readonly mcpExclusions: readonly McpExclusion[] = [],\n ) {}\n\n // webpieces-disable no-function-outside-class -- static factory of this class\n static empty(): ApiDocRulesFindings {\n return new ApiDocRulesFindings(\n new ApiRuleFindings([], []),\n new ApiRuleFindings([], []),\n [],\n );\n }\n}\n\n/** The mode value that narrows the scan to the projects the diff touched. */\nexport const AFFECTED_PROJECT_MODE = 'AFFECTED_PROJECT';\n\n/**\n * ONE rule's switches, resolved from webpieces.config.json once per scan.\n *\n * There is NO default (#1017). Both rules are schema'd, so `webpieces.config.json` must carry an\n * entry for each or the config FAILS TO LOAD naming them — a consumer states `OFF`,\n * `AFFECTED_PROJECT` or `RUN_EVERY_TIME` out loud. The `off()` a bare constructor or a unit test\n * gets is not a default; it is what a caller that configured nothing asked for.\n */\nexport class ApiDocRule {\n constructor(\n public readonly name: string,\n public readonly enabled: boolean,\n /** Project roots this rule does not apply to — `allowedPaths` in the config. */\n public readonly allowedPaths: readonly string[],\n /**\n * Workspace-relative paths the diff touched, under `AFFECTED_PROJECT`; `null` under\n * `RUN_EVERY_TIME`, which means every project is in scope.\n *\n * `null` is also what an UNCOMPUTABLE diff resolves to — no merge-base, a shallow clone, no\n * repository at all. A diff that could not be read is not evidence that nothing changed, and\n * the only safe reading of \"I do not know what changed\" is \"look at all of it\".\n */\n public readonly changedPaths: readonly string[] | null = null,\n ) {}\n\n /** ARMED over every project — what a unit test constructs, and what RUN_EVERY_TIME resolves to. */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static armed(name: string): ApiDocRule {\n return new ApiDocRule(name, true, [], null);\n }\n\n /** ARMED over the projects owning one of `changedPaths` — what AFFECTED_PROJECT resolves to. */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static affected(name: string, changedPaths: readonly string[]): ApiDocRule {\n return new ApiDocRule(name, true, [], changedPaths);\n }\n\n // webpieces-disable no-function-outside-class -- static factory of this class\n static off(name: string): ApiDocRule {\n return new ApiDocRule(name, false, [], null);\n }\n\n /**\n * True when this rule scans the contracts of the project rooted at `root`.\n *\n * ONE place answers it, for both rules, so `allowedPaths` and the affected-project narrowing can\n * never be applied by one caller and skipped by another.\n */\n coversProject(root: string): boolean {\n if (!this.enabled) return false;\n if (matchesAnyGlob(root, this.allowedPaths)) return false;\n if (this.changedPaths === null) return true;\n const prefix = `${root}/`;\n return this.changedPaths.some((each: string): boolean => each.startsWith(prefix));\n }\n\n /** `mode: OFF` and the time-box/branch hatches come from RuleGate, so there is ONE reading. */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static fromConfig(workspaceRoot: string, name: string): ApiDocRule {\n if (new RuleGate().isDisabled(workspaceRoot, name, true)) {\n return ApiDocRule.off(name);\n }\n const rule = loadAndValidate(workspaceRoot).resolved.rules.get(name);\n const allowed = rule?.options['allowedPaths'];\n const allowedPaths = Array.isArray(allowed) ? (allowed as string[]) : [];\n const affected =\n rule?.options['mode'] === AFFECTED_PROJECT_MODE\n ? ApiDocRule.changedPathsOf(workspaceRoot)\n : null;\n return new ApiDocRule(name, true, allowedPaths, affected);\n }\n\n /**\n * Every path the diff touched, against the base nx itself uses (`NX_BASE`, else the merge-base\n * with origin/main). NOT ts-only and INCLUDING deletions: a deleted DTO changes what a project's\n * contracts can express exactly as an added one does, and a `project.json` edit can change which\n * project owns a contract at all.\n */\n // webpieces-disable no-function-outside-class -- private static reader of this class\n private static changedPathsOf(workspaceRoot: string): readonly string[] | null {\n const scope = new DiffScope();\n const range = scope.resolveBase(workspaceRoot);\n if (range.base === undefined) return null;\n const opts = new ChangedFilesOptions();\n opts.tsOnly = false;\n opts.includeDeletions = true;\n return scope.getChangedFiles(workspaceRoot, range.base, range.head, opts);\n }\n}\n\n/** ONE `webpieces-disable` naming a rule, and whether it gave a reason. */\nexport class DisableComment {\n constructor(public readonly hasReason: boolean) {}\n\n /**\n * The disable naming `rule` on the declaration at `line` (1-based) of `file`, or undefined.\n *\n * It walks UPWARD over the declaration's own leading trivia — blank lines, `//` comments, a\n * JSDoc block, and the decorators between them — because that is where an author writes one, and\n * it stops at the first line that is none of those, so a directive belonging to the PREVIOUS\n * declaration can never be read as covering this one.\n *\n * Only a directive NAMING this rule counts. An existing `// webpieces-disable no-any-unknown`\n * therefore does not silence these rules, which is the point: that rule answers \"is this\n * type-safe?\" and these answer \"is this field PUBLISHED with no shape?\" — different questions\n * with different right answers, so the second one wants its own, separately argued line.\n *\n * A file that cannot be read yields \"no disable\": a defect is still a defect, and inventing a\n * suppression out of an I/O failure is the one wrong answer.\n */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static readAt(file: string, line: number, rule: string): DisableComment | undefined {\n const lines = DisableComment.linesOf(file);\n if (lines === undefined) return undefined;\n const start = Math.max(0, line - 1);\n const onDeclaration = DisableComment.match(lines[start] ?? '', rule);\n if (onDeclaration !== undefined) return onDeclaration;\n for (let i = start - 1; i >= 0 && i >= start - DISABLE_LOOKBACK_LINES; i--) {\n const text = (lines[i] ?? '').trim();\n const above = DisableComment.match(text, rule);\n if (above !== undefined) return above;\n if (!DisableComment.isTrivia(text)) return undefined;\n }\n return undefined;\n }\n\n /** Leading trivia a disable comment is allowed to sit above: blanks, comments and decorators. */\n // webpieces-disable no-function-outside-class -- private static predicate of this class\n private static isTrivia(text: string): boolean {\n return (\n text === '' ||\n text.startsWith('//') ||\n text.startsWith('*') ||\n text.startsWith('/*') ||\n text.startsWith('@')\n );\n }\n\n // webpieces-disable no-function-outside-class -- private static reader of this class\n private static match(text: string, rule: string): DisableComment | undefined {\n const found = text.trim().match(DISABLE_RE);\n if (found === null) return undefined;\n const named = found[1].split(',').map((each: string): string => each.trim());\n if (!named.includes(rule)) return undefined;\n return new DisableComment((found[2] ?? '').trim() !== '');\n }\n\n /** The file's lines, or undefined when it cannot be read. */\n // webpieces-disable no-function-outside-class -- private static reader of this class\n private static linesOf(file: string): string[] | undefined {\n if (!fs.existsSync(file)) return undefined;\n return fs.readFileSync(file, 'utf8').split('\\n');\n }\n}\n"]}
@@ -31,6 +31,7 @@ import type { EnhancedGraph } from '../graph-sorter';
31
31
  import { ProjectInfo } from '../project-info';
32
32
  import { ApiClassInfo, ApiContracts, EmptiedApiContract, NonLiteralDecoratorArg, ProjectApiRelations, UndeclaredExternalCaller, UndeclaredEndpointOperation, UnresolvedApiCall, UnresolvedEndpointPath } from './api-relations';
33
33
  import { RootUnionFindings, RootUnionRule } from './root-union-scan';
34
+ import { ApiDocRule, ApiDocRulesFindings } from './api-doc-rules';
34
35
  /** The whole-workspace result of a scan. */
35
36
  export interface ApiScanResult {
36
37
  /** projectName -> { apiLibProject -> relation }; only projects with ≥1 relation appear. */
@@ -76,6 +77,11 @@ export interface ApiScanResult {
76
77
  undeclaredEndpointOperations: UndeclaredEndpointOperation[];
77
78
  /** `no-root-union-api-type`'s findings. Fatal in buildApiContracts — see root-union-scan.ts. */
78
79
  rootUnions: RootUnionFindings;
80
+ /**
81
+ * `api-rules-for-openapi` / `api-rules-for-mcp` findings (#1011). Both ship OFF, so this is
82
+ * EMPTY unless a repo stated otherwise — no rule in this framework has a default (#1017).
83
+ */
84
+ apiDocRules: ApiDocRulesFindings;
79
85
  }
80
86
  /** Statically scans every project for its api-lib implements/uses relationships. */
81
87
  export declare class ApiUsageScanner {
@@ -85,6 +91,10 @@ export declare class ApiUsageScanner {
85
91
  private readonly externalApiPaths;
86
92
  /** `no-root-union-api-type`'s switches — ARMED unless scanAndAttachApiRelations read otherwise. */
87
93
  private readonly rootUnionRule;
94
+ /** `api-rules-for-openapi`'s switches — read from the config, which MUST state them (#1017). */
95
+ private readonly openApiRule;
96
+ /** `api-rules-for-mcp`'s switches — read from the config, which MUST state them (#1017). */
97
+ private readonly mcpRule;
88
98
  private readonly locator;
89
99
  private readonly relationsByProject;
90
100
  private readonly scannedProjects;
@@ -95,7 +105,11 @@ export declare class ApiUsageScanner {
95
105
  /** Globs of project roots whose exported `*Api` types are contracts for outside systems. */
96
106
  externalApiPaths?: readonly string[],
97
107
  /** `no-root-union-api-type`'s switches — ARMED unless scanAndAttachApiRelations read otherwise. */
98
- rootUnionRule?: RootUnionRule);
108
+ rootUnionRule?: RootUnionRule,
109
+ /** `api-rules-for-openapi`'s switches — read from the config, which MUST state them (#1017). */
110
+ openApiRule?: ApiDocRule,
111
+ /** `api-rules-for-mcp`'s switches — read from the config, which MUST state them (#1017). */
112
+ mcpRule?: ApiDocRule);
99
113
  scan(): ApiScanResult;
100
114
  private scanProject;
101
115
  private visit;
@@ -44,6 +44,8 @@ const bindings_1 = require("../di-graph/bindings");
44
44
  const api_relations_1 = require("./api-relations");
45
45
  const api_contract_errors_1 = require("./api-contract-errors");
46
46
  const root_union_scan_1 = require("./root-union-scan");
47
+ const api_doc_rules_1 = require("./api-doc-rules");
48
+ const api_doc_rules_scan_1 = require("./api-doc-rules-scan");
47
49
  const api_ast_1 = require("./api-ast");
48
50
  const RPC_CLIENT_METHOD = 'createRpcClient';
49
51
  const PUBSUB_CLIENT_METHOD = 'createPubSubClient';
@@ -206,6 +208,8 @@ class ApiUsageScanner {
206
208
  projectInfos;
207
209
  externalApiPaths;
208
210
  rootUnionRule;
211
+ openApiRule;
212
+ mcpRule;
209
213
  locator;
210
214
  relationsByProject = new Map();
211
215
  scannedProjects = new Set();
@@ -216,11 +220,17 @@ class ApiUsageScanner {
216
220
  /** Globs of project roots whose exported `*Api` types are contracts for outside systems. */
217
221
  externalApiPaths = [],
218
222
  /** `no-root-union-api-type`'s switches — ARMED unless scanAndAttachApiRelations read otherwise. */
219
- rootUnionRule = root_union_scan_1.RootUnionRule.enabledEverywhere()) {
223
+ rootUnionRule = root_union_scan_1.RootUnionRule.enabledEverywhere(),
224
+ /** `api-rules-for-openapi`'s switches — read from the config, which MUST state them (#1017). */
225
+ openApiRule = api_doc_rules_1.ApiDocRule.off(api_doc_rules_1.OPENAPI_RULE),
226
+ /** `api-rules-for-mcp`'s switches — read from the config, which MUST state them (#1017). */
227
+ mcpRule = api_doc_rules_1.ApiDocRule.off(api_doc_rules_1.MCP_RULE)) {
220
228
  this.workspaceRoot = workspaceRoot;
221
229
  this.projectInfos = projectInfos;
222
230
  this.externalApiPaths = externalApiPaths;
223
231
  this.rootUnionRule = rootUnionRule;
232
+ this.openApiRule = openApiRule;
233
+ this.mcpRule = mcpRule;
224
234
  this.locator = new ProjectLocator(workspaceRoot, projectInfos);
225
235
  this.decoratorArgDiagnostics = new api_ast_1.DecoratorArgDiagnostics(workspaceRoot);
226
236
  }
@@ -245,6 +255,7 @@ class ApiUsageScanner {
245
255
  undeclaredExternalCallers: this.decoratorArgDiagnostics.undeclaredExternalCallers(),
246
256
  undeclaredEndpointOperations: this.decoratorArgDiagnostics.undeclaredEndpointOperations(),
247
257
  rootUnions: new root_union_scan_1.RootUnionScan(this.workspaceRoot, this.projectInfos, this.rootUnionRule).run(),
258
+ apiDocRules: new api_doc_rules_scan_1.ApiDocRulesScan(this.workspaceRoot, this.projectInfos, this.openApiRule, this.mcpRule).run(),
248
259
  };
249
260
  }
250
261
  scanProject(info) {
@@ -389,7 +400,7 @@ exports.ApiUsageScanner = ApiUsageScanner;
389
400
  */
390
401
  // webpieces-disable no-function-outside-class -- module entry point, mirrors generateReducedGraph/collectBindings
391
402
  function scanAndAttachApiRelations(workspaceRoot, graph, projectInfos, externalApiPaths = []) {
392
- const result = new ApiUsageScanner(workspaceRoot, projectInfos, externalApiPaths, root_union_scan_1.RootUnionRule.fromConfig(workspaceRoot)).scan();
403
+ const result = new ApiUsageScanner(workspaceRoot, projectInfos, externalApiPaths, root_union_scan_1.RootUnionRule.fromConfig(workspaceRoot), api_doc_rules_1.ApiDocRule.fromConfig(workspaceRoot, api_doc_rules_1.OPENAPI_RULE), api_doc_rules_1.ApiDocRule.fromConfig(workspaceRoot, api_doc_rules_1.MCP_RULE)).scan();
393
404
  for (const projectName of result.relationsByProject.keys()) {
394
405
  const entry = graph[projectName];
395
406
  if (entry)
@@ -432,6 +443,15 @@ function buildApiContracts(scan) {
432
443
  // are contracts the scan could not READ at all.
433
444
  if (!scan.rootUnions.isEmpty())
434
445
  throw new api_contract_errors_1.RootUnionApiTypeError(scan.rootUnions);
446
+ // Contract shapes that are not PUBLISHABLE, read by the generator's own extractor (#1011).
447
+ // After the root union, which is the one shape no function-calling API will accept at all, and
448
+ // before the caller checks below, which are about the architecture graph rather than a document.
449
+ if (!scan.apiDocRules.openApi.isEmpty()) {
450
+ throw new api_contract_errors_1.ApiRulesForOpenApiError(scan.apiDocRules.openApi);
451
+ }
452
+ if (!scan.apiDocRules.mcp.isEmpty()) {
453
+ throw new api_contract_errors_1.ApiRulesForMcpError(scan.apiDocRules.mcp, scan.apiDocRules.mcpExclusions);
454
+ }
435
455
  // After the two above: an unreadable path is what empties a contract, and a contract that lost
436
456
  // every method has no external endpoint left to complain about.
437
457
  if (scan.undeclaredExternalCallers.length > 0) {