@webpieces/nx-webpieces-rules 0.4.795 → 0.4.796

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/nx-webpieces-rules",
3
- "version": "0.4.795",
3
+ "version": "0.4.796",
4
4
  "description": "Nx-specific webpieces validation rules and graph tooling. Bundles all @webpieces rule packages with Nx graph validators and an inference plugin.",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -18,11 +18,11 @@
18
18
  "README.md"
19
19
  ],
20
20
  "dependencies": {
21
- "@webpieces/ai-hook-rules": "0.4.795",
22
- "@webpieces/code-rules": "0.4.795",
23
- "@webpieces/eslint-rules": "0.4.795",
24
- "@webpieces/pr-gate": "0.4.795",
25
- "@webpieces/rules-config": "0.4.795",
21
+ "@webpieces/ai-hook-rules": "0.4.796",
22
+ "@webpieces/code-rules": "0.4.796",
23
+ "@webpieces/eslint-rules": "0.4.796",
24
+ "@webpieces/pr-gate": "0.4.796",
25
+ "@webpieces/rules-config": "0.4.796",
26
26
  "madge": "8.0.0"
27
27
  },
28
28
  "peerDependencies": {
@@ -1,30 +1,7 @@
1
- /**
2
- * API contract AST accessors
3
- *
4
- * The pure, stateless half of the api scan: given a TypeScript node, what contract / endpoint /
5
- * injected type does it describe? Split out of api-scanner.ts, which owns the STATEFUL walk (project
6
- * programs, the source index, relation accumulation) and had grown past the file-size limit.
7
- *
8
- * Everything here is parser-level on purpose. Decorators must be read exactly as written, and a
9
- * plain parse cannot be diverted to a decorator-erased `.d.ts` by module resolution — the bug
10
- * api-scanner's source pre-pass exists to guard against.
11
- */
1
+ /** Parser-only API contract AST accessors; deliberately avoids module resolution and erased d.ts. */
12
2
  import * as ts from 'typescript';
13
- import { ApiClassInfo, ApiMethodMeta, ApiParameterMeta, ContractHttpMethod, ApiTransport, EmptiedApiContract, ExternalSystemDeclaration, NonLiteralDecoratorArg, UndeclaredExternalCaller, UnresolvedEndpointPath } from './api-relations';
14
- /**
15
- * The module-scope `const NAME = '<string literal>'` bindings of ONE source file.
16
- *
17
- * A contract that hoists its route to a constant (`@ApiPath(WHATSAPP_API_PATH)`) is good practice —
18
- * it lets a sibling contract and its callers share the symbol — but a decorator argument is read as
19
- * TEXT here, with no checker to constant-fold it. Without this table such an argument resolved to
20
- * nothing: the class lost its basePath, and a class whose every @Endpoint path was a constant
21
- * resolved to zero methods and was dropped from the graph entirely.
22
- *
23
- * Deliberately SAME-MODULE only. Following an import would mean resolving modules, which is exactly
24
- * what the source pre-pass avoids (it can be diverted to a decorator-erased `.d.ts`). A cross-module
25
- * constant is therefore still unresolvable — and is REPORTED rather than silently dropped, see
26
- * DecoratorArgDiagnostics.
27
- */
3
+ import { ApiClassInfo, ApiMethodMeta, ApiParameterMeta, ContractHttpMethod, ApiTransport, EmptiedApiContract, EndpointOperation, ExternalSystemDeclaration, NonLiteralDecoratorArg, UndeclaredExternalCaller, UndeclaredEndpointOperation, UnresolvedEndpointPath } from './api-relations';
4
+ /** Same-module string constants that parser-only decorator scanning can safely resolve. */
28
5
  export declare class ModuleStringConstants {
29
6
  private readonly byName;
30
7
  constructor(byName: Map<string, string>);
@@ -34,14 +11,7 @@ export declare class ModuleStringConstants {
34
11
  export declare function stringConstantsOf(sourceFile: ts.SourceFile): ModuleStringConstants;
35
12
  /** The string an initializer denotes, unwrapping `as const` / parentheses, else null. */
36
13
  export declare function stringValueOf(expr: ts.Expression | undefined): string | null;
37
- /**
38
- * ONE decorator argument that had to be a string, and what came of it.
39
- *
40
- * `value` is the string when it was a literal or resolved through a same-module constant.
41
- * `unresolvedName` is the argument as written (`WHATSAPP_API_PATH`) when it is present but could not
42
- * be reduced — the case that must be reported, never silently dropped. Both are null when the
43
- * argument is simply absent.
44
- */
14
+ /** A resolved decorator string, or the source spelling that could not be reduced. */
45
15
  export declare class DecoratorArgValue {
46
16
  readonly value: string | null;
47
17
  readonly unresolvedName: string | null;
@@ -49,26 +19,14 @@ export declare class DecoratorArgValue {
49
19
  }
50
20
  /** Read one decorator argument as a string, resolving same-module constants. */
51
21
  export declare function decoratorArgValue(expr: ts.Expression | undefined, constants: ModuleStringConstants): DecoratorArgValue;
52
- /**
53
- * Collects everything this parser-only pass had to drop: decorator arguments it could not reduce to
54
- * a string, plus the two of those that are FATAL rather than merely lossy.
55
- *
56
- * A same-module constant now resolves, but a cross-module one (`import { PATH } from './paths'`)
57
- * genuinely cannot — the source pre-pass has no checker by design. That gap used to be invisible:
58
- * the contract simply came out with no basePath, or with fewer methods, or not at all. Recording it
59
- * turns a silent drop into a named one, pointing at the exact file, line and identifier.
60
- *
61
- * Three sinks, because the consequences differ. `record` is the warning stream (a @Queue name falls
62
- * back to a derived one, so the graph is degraded, not wrong). `recordUnresolvedPath` and
63
- * `recordEmptiedContract` are collected so generation can FAIL — one aggregated error naming every
64
- * offender, because an author fixing five constants wants all five in one run.
65
- */
22
+ /** Aggregates parser-only scan losses so generation can fail loudly instead of dropping routes. */
66
23
  export declare class DecoratorArgDiagnostics {
67
24
  private readonly workspaceRoot;
68
25
  private readonly found;
69
26
  private readonly unresolvedPaths;
70
27
  private readonly emptied;
71
28
  private readonly undeclaredCallers;
29
+ private readonly undeclaredOperations;
72
30
  constructor(workspaceRoot: string);
73
31
  /** Record `argument` (as written) as unresolvable at `node`'s location. */
74
32
  record(api: string, decorator: string, method: string | null, argument: string, node: ts.Node): void;
@@ -78,43 +36,29 @@ export declare class DecoratorArgDiagnostics {
78
36
  recordEmptiedContract(api: string, declared: number, node: ts.Node): void;
79
37
  /** Record an `external` `@Endpoint` whose caller is unreadable — fatal, see UndeclaredExternalCallerError. */
80
38
  recordUndeclaredCaller(api: string, method: string, argument: string, node: ts.Node): void;
39
+ /** Record an `@Endpoint` whose required operation is missing or unreadable. */
40
+ recordUndeclaredOperation(api: string, method: string, argument: string, node: ts.Node): void;
81
41
  all(): NonLiteralDecoratorArg[];
82
42
  unresolvedEndpointPaths(): UnresolvedEndpointPath[];
83
43
  emptiedContracts(): EmptiedApiContract[];
84
44
  undeclaredExternalCallers(): UndeclaredExternalCaller[];
45
+ undeclaredEndpointOperations(): UndeclaredEndpointOperation[];
85
46
  private locate;
86
47
  }
87
48
  /** {api, owner: `project`, type} when `cls` is an `abstract class` carrying `@ApiPath`, else null. */
88
49
  export declare function apiClassInfoFrom(cls: ts.ClassDeclaration, project: string, diagnostics?: DecoratorArgDiagnostics | null): ApiClassInfo | null;
89
50
  export declare function apiTransport(cls: ts.ClassDeclaration): ApiTransport;
90
- /**
91
- * Every `@Endpoint(path, kind)` method on a contract class, in declaration order.
92
- *
93
- * `kind` is a REQUIRED argument of the decorator, so a missing/non-literal second argument means the
94
- * source does not compile (or is mid-edit) — we skip the method rather than defaulting it. Defaulting
95
- * would put an undeclared cron or webhook into the graph as an ordinary rpc call, which is precisely
96
- * the blindness the required argument exists to remove.
97
- *
98
- * `path` is NOT skippable. It may be a same-module constant; an argument that is present but still
99
- * cannot be reduced is recorded on `diagnostics` as an UnresolvedEndpointPath, which FAILS generation
100
- * later. Upstream components need the URL — a client computes its request as `basePath + path` — so
101
- * dropping the method here shipped a contract missing routing information, and a class whose every
102
- * path was a constant lost every method and disappeared from the graph entirely.
103
- *
104
- * A class that declared endpoints and kept NONE of them is recorded too: `buildApiContracts` skips
105
- * zero-method classes, which is the door a gutted contract used to leave through unannounced.
106
- */
51
+ /** Reads required `(method, path, operation, kind, options?)` endpoint declarations in order. */
107
52
  export declare function endpointMethodsOf(cls: ts.ClassDeclaration, api: string, constants?: ModuleStringConstants, diagnostics?: DecoratorArgDiagnostics | null): ApiMethodMeta[];
108
- /** `@Endpoint(..., { httpMethod: 'GET' })`, defaulting to the runtime's POST default. */
109
- export declare function httpMethodOf(options: ts.Expression | undefined, constants: ModuleStringConstants, diagnostics: DecoratorArgDiagnostics | null, api: string, method: string, node: ts.Node): ContractHttpMethod;
53
+ /** Required side-effect declaration; no inference from GET/POST is permitted. */
54
+ export declare function endpointOperationOf(operationArgument: ts.Expression | undefined, constants: ModuleStringConstants, diagnostics: DecoratorArgDiagnostics | null, api: string, method: string, node: ts.Node): EndpointOperation | null;
55
+ /** Required first positional HTTP method. */
56
+ export declare function httpMethodOf(methodArgument: ts.Expression | undefined, constants: ModuleStringConstants, diagnostics: DecoratorArgDiagnostics | null, api: string, method: string, node: ts.Node): ContractHttpMethod | null;
110
57
  /** Only the non-default full-response marker needs an architecture field. */
111
58
  export declare function endpointResponseTypeOf(options: ts.Expression | undefined, constants: ModuleStringConstants): 'body' | 'full';
112
59
  /** Explicit `@PathParam` / `@QueryParam` mappings in source declaration order. */
113
60
  export declare function httpParametersOf(member: ts.MethodDeclaration, httpMethod: ContractHttpMethod, constants: ModuleStringConstants, diagnostics: DecoratorArgDiagnostics | null, api: string, method: string): ApiParameterMeta[];
114
- /**
115
- * The outcome of reading `@Endpoint(path, 'external', { calledBy, callerKind })`'s third argument:
116
- * either the resolved declaration, or the reason it could not be resolved (never both).
117
- */
61
+ /** Resolved EXTERNAL caller declaration, or the source problem that prevented resolution. */
118
62
  export declare class ExternalCallerRead {
119
63
  readonly declaration: ExternalSystemDeclaration | null;
120
64
  /** What was wrong, as written, for the diagnostic. Null exactly when `declaration` is set. */
@@ -123,15 +67,7 @@ export declare class ExternalCallerRead {
123
67
  /** What was wrong, as written, for the diagnostic. Null exactly when `declaration` is set. */
124
68
  problem: string | null);
125
69
  }
126
- /**
127
- * Read the declared caller out of the @Endpoint OPTIONS OBJECT LITERAL — `args[2]`, not a positional
128
- * argument, because that is where `formPost` already lives and one options bag beats two.
129
- *
130
- * Everything unreadable is a PROBLEM, never a default: an unknown `callerKind` draws the wrong shape
131
- * (which teaches the reader something false), and a missing `calledBy` puts us back at a box that can
132
- * only name our own contract. The kind default applies ONLY to the case the API deliberately allows —
133
- * `calledBy` present, `callerKind` absent.
134
- */
70
+ /** Reads `calledBy` and optional `callerKind` from the fifth positional options argument. */
135
71
  export declare function externalCallerOf(arg: ts.Expression | undefined, constants: ModuleStringConstants): ExternalCallerRead;
136
72
  /** One property of an object literal, read as a string through the same constant folding as an argument. */
137
73
  export declare function objectPropertyValue(literal: ts.ObjectLiteralExpression, name: string, constants: ModuleStringConstants): DecoratorArgValue;
@@ -1,15 +1,5 @@
1
1
  "use strict";
2
- /**
3
- * API contract AST accessors
4
- *
5
- * The pure, stateless half of the api scan: given a TypeScript node, what contract / endpoint /
6
- * injected type does it describe? Split out of api-scanner.ts, which owns the STATEFUL walk (project
7
- * programs, the source index, relation accumulation) and had grown past the file-size limit.
8
- *
9
- * Everything here is parser-level on purpose. Decorators must be read exactly as written, and a
10
- * plain parse cannot be diverted to a decorator-erased `.d.ts` by module resolution — the bug
11
- * api-scanner's source pre-pass exists to guard against.
12
- */
2
+ /** Parser-only API contract AST accessors; deliberately avoids module resolution and erased d.ts. */
13
3
  Object.defineProperty(exports, "__esModule", { value: true });
14
4
  exports.ExternalCallerRead = exports.DecoratorArgDiagnostics = exports.DecoratorArgValue = exports.ModuleStringConstants = void 0;
15
5
  exports.stringConstantsOf = stringConstantsOf;
@@ -18,6 +8,7 @@ exports.decoratorArgValue = decoratorArgValue;
18
8
  exports.apiClassInfoFrom = apiClassInfoFrom;
19
9
  exports.apiTransport = apiTransport;
20
10
  exports.endpointMethodsOf = endpointMethodsOf;
11
+ exports.endpointOperationOf = endpointOperationOf;
21
12
  exports.httpMethodOf = httpMethodOf;
22
13
  exports.endpointResponseTypeOf = endpointResponseTypeOf;
23
14
  exports.httpParametersOf = httpParametersOf;
@@ -48,8 +39,20 @@ const fs = tslib_1.__importStar(require("fs"));
48
39
  const path = tslib_1.__importStar(require("path"));
49
40
  const bindings_1 = require("../di-graph/bindings");
50
41
  const api_relations_1 = require("./api-relations");
51
- /** Legal `@Endpoint(path, kind)` values; anything else is a source error, not a kind we invent. */
42
+ /** Legal enum-backed `@Endpoint` symbols; tooling reads source without importing application code. */
52
43
  const ENDPOINT_KINDS = ['rpc', 'cloudtasks', 'cron', 'external'];
44
+ const ENDPOINT_OPERATIONS = ['read', 'write-idempotent', 'write'];
45
+ const ENDPOINT_SYMBOLS = {
46
+ GET: 'GET',
47
+ POST: 'POST',
48
+ READ: 'read',
49
+ WRITE_IDEMPOTENT: 'write-idempotent',
50
+ WRITE: 'write',
51
+ RPC: 'rpc',
52
+ CLOUDTASKS: 'cloudtasks',
53
+ CRON: 'cron',
54
+ EXTERNAL: 'external',
55
+ };
53
56
  /**
54
57
  * Name suffix that marks an exported type in an `externalApiPaths` project as a vendor CONTRACT
55
58
  * (`GmailApi`, `StorageApi`) rather than one of the DTOs, configs or clients sitting beside it.
@@ -64,20 +67,7 @@ const EXTERNAL_SYSTEM_TAG = 'externalSystem';
64
67
  * `svcName` first, and a consumer's own `XxxClientConfig` follows the same shape.
65
68
  */
66
69
  const CLIENT_CONFIG_SUFFIX = 'ClientConfig';
67
- /**
68
- * The module-scope `const NAME = '<string literal>'` bindings of ONE source file.
69
- *
70
- * A contract that hoists its route to a constant (`@ApiPath(WHATSAPP_API_PATH)`) is good practice —
71
- * it lets a sibling contract and its callers share the symbol — but a decorator argument is read as
72
- * TEXT here, with no checker to constant-fold it. Without this table such an argument resolved to
73
- * nothing: the class lost its basePath, and a class whose every @Endpoint path was a constant
74
- * resolved to zero methods and was dropped from the graph entirely.
75
- *
76
- * Deliberately SAME-MODULE only. Following an import would mean resolving modules, which is exactly
77
- * what the source pre-pass avoids (it can be diverted to a decorator-erased `.d.ts`). A cross-module
78
- * constant is therefore still unresolvable — and is REPORTED rather than silently dropped, see
79
- * DecoratorArgDiagnostics.
80
- */
70
+ /** Same-module string constants that parser-only decorator scanning can safely resolve. */
81
71
  class ModuleStringConstants {
82
72
  byName;
83
73
  constructor(byName) {
@@ -125,14 +115,7 @@ function stringValueOf(expr) {
125
115
  return stringValueOf(expr.expression);
126
116
  return null;
127
117
  }
128
- /**
129
- * ONE decorator argument that had to be a string, and what came of it.
130
- *
131
- * `value` is the string when it was a literal or resolved through a same-module constant.
132
- * `unresolvedName` is the argument as written (`WHATSAPP_API_PATH`) when it is present but could not
133
- * be reduced — the case that must be reported, never silently dropped. Both are null when the
134
- * argument is simply absent.
135
- */
118
+ /** A resolved decorator string, or the source spelling that could not be reduced. */
136
119
  class DecoratorArgValue {
137
120
  value;
138
121
  unresolvedName;
@@ -158,26 +141,24 @@ function decoratorArgValue(expr, constants) {
158
141
  }
159
142
  return new DecoratorArgValue(null, expr.getText());
160
143
  }
161
- /**
162
- * Collects everything this parser-only pass had to drop: decorator arguments it could not reduce to
163
- * a string, plus the two of those that are FATAL rather than merely lossy.
164
- *
165
- * A same-module constant now resolves, but a cross-module one (`import { PATH } from './paths'`)
166
- * genuinely cannot — the source pre-pass has no checker by design. That gap used to be invisible:
167
- * the contract simply came out with no basePath, or with fewer methods, or not at all. Recording it
168
- * turns a silent drop into a named one, pointing at the exact file, line and identifier.
169
- *
170
- * Three sinks, because the consequences differ. `record` is the warning stream (a @Queue name falls
171
- * back to a derived one, so the graph is degraded, not wrong). `recordUnresolvedPath` and
172
- * `recordEmptiedContract` are collected so generation can FAIL — one aggregated error naming every
173
- * offender, because an author fixing five constants wants all five in one run.
174
- */
144
+ /** Resolve the enum-backed constants intentionally allowed in @Endpoint positional arguments. */
145
+ // webpieces-disable no-function-outside-class -- pure AST accessor for decorator source
146
+ function endpointSymbolArgValue(expr, constants) {
147
+ if (expr && ts.isIdentifier(expr)) {
148
+ const builtin = ENDPOINT_SYMBOLS[expr.text];
149
+ if (builtin !== undefined)
150
+ return new DecoratorArgValue(builtin, null);
151
+ }
152
+ return decoratorArgValue(expr, constants);
153
+ }
154
+ /** Aggregates parser-only scan losses so generation can fail loudly instead of dropping routes. */
175
155
  class DecoratorArgDiagnostics {
176
156
  workspaceRoot;
177
157
  found = [];
178
158
  unresolvedPaths = [];
179
159
  emptied = [];
180
160
  undeclaredCallers = [];
161
+ undeclaredOperations = [];
181
162
  constructor(workspaceRoot) {
182
163
  this.workspaceRoot = workspaceRoot;
183
164
  }
@@ -197,6 +178,10 @@ class DecoratorArgDiagnostics {
197
178
  recordUndeclaredCaller(api, method, argument, node) {
198
179
  this.undeclaredCallers.push(new api_relations_1.UndeclaredExternalCaller(api, method, argument, this.locate(node)));
199
180
  }
181
+ /** Record an `@Endpoint` whose required operation is missing or unreadable. */
182
+ recordUndeclaredOperation(api, method, argument, node) {
183
+ this.undeclaredOperations.push(new api_relations_1.UndeclaredEndpointOperation(api, method, argument, this.locate(node)));
184
+ }
200
185
  all() {
201
186
  return this.found;
202
187
  }
@@ -209,6 +194,9 @@ class DecoratorArgDiagnostics {
209
194
  undeclaredExternalCallers() {
210
195
  return this.undeclaredCallers;
211
196
  }
197
+ undeclaredEndpointOperations() {
198
+ return this.undeclaredOperations;
199
+ }
212
200
  locate(node) {
213
201
  const sourceFile = node.getSourceFile();
214
202
  const position = sourceFile.getLineAndCharacterOfPosition(node.getStart());
@@ -240,23 +228,7 @@ function apiTransport(cls) {
240
228
  }
241
229
  /** The @Endpoint kinds that are actually DELIVERED through a named queue or schedule. */
242
230
  const QUEUED_KINDS = ['cloudtasks', 'cron'];
243
- /**
244
- * Every `@Endpoint(path, kind)` method on a contract class, in declaration order.
245
- *
246
- * `kind` is a REQUIRED argument of the decorator, so a missing/non-literal second argument means the
247
- * source does not compile (or is mid-edit) — we skip the method rather than defaulting it. Defaulting
248
- * would put an undeclared cron or webhook into the graph as an ordinary rpc call, which is precisely
249
- * the blindness the required argument exists to remove.
250
- *
251
- * `path` is NOT skippable. It may be a same-module constant; an argument that is present but still
252
- * cannot be reduced is recorded on `diagnostics` as an UnresolvedEndpointPath, which FAILS generation
253
- * later. Upstream components need the URL — a client computes its request as `basePath + path` — so
254
- * dropping the method here shipped a contract missing routing information, and a class whose every
255
- * path was a constant lost every method and disappeared from the graph entirely.
256
- *
257
- * A class that declared endpoints and kept NONE of them is recorded too: `buildApiContracts` skips
258
- * zero-method classes, which is the door a gutted contract used to leave through unannounced.
259
- */
231
+ /** Reads required `(method, path, operation, kind, options?)` endpoint declarations in order. */
260
232
  // webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts
261
233
  function endpointMethodsOf(cls, api, constants = new ModuleStringConstants(new Map()), diagnostics = null) {
262
234
  const methods = [];
@@ -270,41 +242,43 @@ function endpointMethodsOf(cls, api, constants = new ModuleStringConstants(new M
270
242
  declared++;
271
243
  const name = member.name.text;
272
244
  const args = decoratorArgs(endpoint);
273
- const pathArg = decoratorArgValue(args[0], constants);
274
- const kindArg = decoratorArgValue(args[1], constants);
245
+ const methodArg = endpointSymbolArgValue(args[0], constants);
246
+ const pathArg = decoratorArgValue(args[1], constants);
247
+ const operationArg = endpointSymbolArgValue(args[2], constants);
248
+ const kindArg = endpointSymbolArgValue(args[3], constants);
249
+ reportUnresolved(diagnostics, api, 'Endpoint', name, methodArg, endpoint);
275
250
  reportUnresolved(diagnostics, api, 'Endpoint', name, pathArg, endpoint);
251
+ reportUnresolved(diagnostics, api, 'Endpoint', name, operationArg, endpoint);
276
252
  reportUnresolved(diagnostics, api, 'Endpoint', name, kindArg, endpoint);
277
253
  if (diagnostics !== null && pathArg.unresolvedName !== null) {
278
254
  diagnostics.recordUnresolvedPath(api, name, pathArg.unresolvedName, endpoint);
279
255
  }
256
+ const httpMethod = methodArg.value;
257
+ const operation = endpointOperationOf(args[2], constants, diagnostics, api, name, endpoint);
280
258
  const kind = kindArg.value;
281
- if (pathArg.value === null ||
259
+ if ((httpMethod !== 'GET' && httpMethod !== 'POST') ||
260
+ pathArg.value === null ||
261
+ operation === null ||
282
262
  kind === null ||
283
263
  !ENDPOINT_KINDS.includes(kind))
284
264
  continue;
285
- const httpMethod = httpMethodOf(args[2], constants, diagnostics, api, name, endpoint);
286
265
  const method = {
287
266
  name,
288
267
  path: pathArg.value,
289
268
  kind: kind,
269
+ operation,
290
270
  httpMethod,
291
271
  };
292
272
  const parameters = httpParametersOf(member, httpMethod, constants, diagnostics, api, name);
293
273
  if (parameters.length > 0)
294
274
  method.parameters = parameters;
295
- if (endpointResponseTypeOf(args[2], constants) === 'full')
275
+ if (endpointResponseTypeOf(args[4], constants) === 'full')
296
276
  method.responseType = 'full';
297
- // Only a queued or scheduled endpoint HAS a queue. Naming one for a synchronous rpc invited a
298
- // tool to read `methods.map(m => m.queueName)` as a provisioning list and create queues that
299
- // nothing will ever deliver to.
300
277
  if (QUEUED_KINDS.includes(method.kind)) {
301
278
  method.queueName = queueNameOf(member, api, name, constants, diagnostics);
302
279
  }
303
- // Only an `external` endpoint HAS an outside caller, mirroring the queue rule above. A
304
- // caller recorded on an rpc method would be a fact about nothing, and would put a vendor
305
- // box on the graph beside an endpoint no vendor calls.
306
280
  if (method.kind === 'external') {
307
- const caller = externalCallerOf(args[2], constants);
281
+ const caller = externalCallerOf(args[4], constants);
308
282
  if (caller.declaration !== null)
309
283
  method.caller = caller.declaration;
310
284
  else if (diagnostics !== null)
@@ -312,21 +286,35 @@ function endpointMethodsOf(cls, api, constants = new ModuleStringConstants(new M
312
286
  }
313
287
  methods.push(method);
314
288
  }
315
- // Declared endpoints, kept none: the class is about to be skipped as "zero methods" and would
316
- // leave no trace. Never legitimate — a routeless contract declares no @Endpoint at all.
317
289
  if (diagnostics !== null && declared > 0 && methods.length === 0) {
318
290
  diagnostics.recordEmptiedContract(api, declared, cls);
319
291
  }
320
292
  return methods;
321
293
  }
322
- /** `@Endpoint(..., { httpMethod: 'GET' })`, defaulting to the runtime's POST default. */
294
+ /** Required side-effect declaration; no inference from GET/POST is permitted. */
323
295
  // webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers
324
- function httpMethodOf(options, constants, diagnostics, api, method, node) {
325
- if (options === undefined || !ts.isObjectLiteralExpression(options))
326
- return 'POST';
327
- const declared = objectPropertyValue(options, 'httpMethod', constants);
296
+ function endpointOperationOf(operationArgument, constants, diagnostics, api, method, node) {
297
+ const declared = endpointSymbolArgValue(operationArgument, constants);
298
+ if (declared.value === null) {
299
+ diagnostics?.recordUndeclaredOperation(api, method, declared.unresolvedName ?? '<missing>', node);
300
+ return null;
301
+ }
302
+ reportUnresolved(diagnostics, api, 'Endpoint.operation', method, declared, node);
303
+ if (declared.value !== null &&
304
+ ENDPOINT_OPERATIONS.includes(declared.value)) {
305
+ return declared.value;
306
+ }
307
+ diagnostics?.recordUndeclaredOperation(api, method, declared.unresolvedName ?? declared.value ?? '<missing>', node);
308
+ return null;
309
+ }
310
+ /** Required first positional HTTP method. */
311
+ // webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers
312
+ function httpMethodOf(methodArgument, constants, diagnostics, api, method, node) {
313
+ const declared = endpointSymbolArgValue(methodArgument, constants);
328
314
  reportUnresolved(diagnostics, api, 'Endpoint.httpMethod', method, declared, node);
329
- return declared.value === 'GET' ? 'GET' : 'POST';
315
+ if (declared.value === 'GET' || declared.value === 'POST')
316
+ return declared.value;
317
+ return null;
330
318
  }
331
319
  /** Only the non-default full-response marker needs an architecture field. */
332
320
  // webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers
@@ -362,10 +350,7 @@ function httpParametersOf(member, httpMethod, constants, diagnostics, api, metho
362
350
  }
363
351
  /** Default `callerKind` when an `external` endpoint declares `calledBy` alone — mirrors core-util. */
364
352
  const DEFAULT_CALLER_KIND = 'saas';
365
- /**
366
- * The outcome of reading `@Endpoint(path, 'external', { calledBy, callerKind })`'s third argument:
367
- * either the resolved declaration, or the reason it could not be resolved (never both).
368
- */
353
+ /** Resolved EXTERNAL caller declaration, or the source problem that prevented resolution. */
369
354
  class ExternalCallerRead {
370
355
  declaration;
371
356
  problem;
@@ -377,15 +362,7 @@ class ExternalCallerRead {
377
362
  }
378
363
  }
379
364
  exports.ExternalCallerRead = ExternalCallerRead;
380
- /**
381
- * Read the declared caller out of the @Endpoint OPTIONS OBJECT LITERAL — `args[2]`, not a positional
382
- * argument, because that is where `formPost` already lives and one options bag beats two.
383
- *
384
- * Everything unreadable is a PROBLEM, never a default: an unknown `callerKind` draws the wrong shape
385
- * (which teaches the reader something false), and a missing `calledBy` puts us back at a box that can
386
- * only name our own contract. The kind default applies ONLY to the case the API deliberately allows —
387
- * `calledBy` present, `callerKind` absent.
388
- */
365
+ /** Reads `calledBy` and optional `callerKind` from the fifth positional options argument. */
389
366
  // webpieces-disable no-function-outside-class -- pure AST accessor, matching the sibling helpers in di-graph/bindings.ts
390
367
  function externalCallerOf(arg, constants) {
391
368
  if (arg === undefined)