carrick 0.3.59 → 0.3.60

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": "carrick",
3
- "version": "0.3.59",
3
+ "version": "0.3.60",
4
4
  "description": "The API contract index for a TypeScript workspace: what the other services do with the routes and calls in the file you are editing, in your editor and in your agent's context",
5
5
  "keywords": [
6
6
  "typescript",
@@ -55,11 +55,11 @@
55
55
  "zod": "^3.23.0"
56
56
  },
57
57
  "optionalDependencies": {
58
- "@carrick-tools/cli-darwin-arm64": "0.3.59",
59
- "@carrick-tools/cli-darwin-x64": "0.3.59",
60
- "@carrick-tools/cli-linux-arm64": "0.3.59",
61
- "@carrick-tools/cli-linux-x64": "0.3.59",
62
- "@carrick-tools/cli-win32-x64": "0.3.59"
58
+ "@carrick-tools/cli-darwin-arm64": "0.3.60",
59
+ "@carrick-tools/cli-darwin-x64": "0.3.60",
60
+ "@carrick-tools/cli-linux-arm64": "0.3.60",
61
+ "@carrick-tools/cli-linux-x64": "0.3.60",
62
+ "@carrick-tools/cli-win32-x64": "0.3.60"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@types/node": "^24.13.3",
@@ -272,6 +272,24 @@ export declare class TypeInferrer {
272
272
  * Check if a type string is "useless" for payload purposes.
273
273
  */
274
274
  private isUselessType;
275
+ /**
276
+ * A type that can be CALLED or constructed is machinery, never a payload
277
+ * (carrick#964).
278
+ *
279
+ * A context-object framework hands the handler one object that both reads the
280
+ * request and sends the response, so that object has a `body` MEMBER whose
281
+ * type is the response sender. Every anchor that reads a member off the
282
+ * handler's parameter, and every locator that lands on such a member, can
283
+ * therefore resolve to a perfectly concrete type that is the wrong side of
284
+ * the exchange — and, being concrete, it sails past the useless-type guard and
285
+ * publishes as an explicit contract.
286
+ *
287
+ * Nothing crosses an HTTP (or queue, or topic) boundary as a function, so the
288
+ * presence of call or construct signatures is a structural, framework-free
289
+ * proof that the resolution landed on machinery. The caller abstains, which
290
+ * leaves the row honestly unresolved instead of confidently wrong.
291
+ */
292
+ private isCallableType;
275
293
  private unwrapExpressionNode;
276
294
  /**
277
295
  * If `node` is a `JSON.stringify(arg)` call, return the (expression-unwrapped)
@@ -797,6 +815,18 @@ export declare class TypeInferrer {
797
815
  * the route declares its request nowhere we can read.
798
816
  */
799
817
  private requestContractFromRegistration;
818
+ /**
819
+ * The request contract a route DECLARES, in anchor order: the handler's
820
+ * parameter annotation (a), the registration's schema object (b), then a
821
+ * validator middleware bound to the request body (b2).
822
+ *
823
+ * Both callers — the registration locator and the expression locator whose
824
+ * line is a registration — consult exactly this set, so the two cannot drift.
825
+ * The typed request READ inside the handler body is deliberately not part of
826
+ * it: that anchor is itself an expression, and the expression path keeps its
827
+ * own locator authoritative when a route declares nothing.
828
+ */
829
+ private declaredRequestContract;
800
830
  /**
801
831
  * Anchor (a): the request contract declared on the handler's own signature.
802
832
  *
@@ -810,6 +840,28 @@ export declare class TypeInferrer {
810
840
  * anchor runs.
811
841
  */
812
842
  private requestBodyFromHandlerParams;
843
+ /**
844
+ * Anchor (b2): the contract declared by a VALIDATOR MIDDLEWARE on the
845
+ * registration (carrick#964).
846
+ *
847
+ * A route can declare its body by binding a schema to a named part of the
848
+ * request in a middleware the registration carries alongside the handler:
849
+ *
850
+ * router.post('/search', validate('json', PayloadSchema), async (c) => …)
851
+ *
852
+ * The shape is a CALL among the registration's arguments whose own arguments
853
+ * are a request part and a schema value. Neither the middleware's name nor
854
+ * the schema library is checked: the part is read off the string literal, and
855
+ * the schema is whatever exposes a parsed output (`schemaOutputTypeText`) —
856
+ * the same test anchor (b) already applies to a `schema: { body: … }` object.
857
+ *
858
+ * `REQUEST_BODY_PARTS` is HTTP vocabulary for how a body is carried, not a
859
+ * framework list. A middleware bound to any other part (`query`, `param`,
860
+ * `header`, `cookie`) declares no BODY, and a middleware that names no part
861
+ * at all is not read: publishing a query schema as the request contract would
862
+ * be the same confident-and-wrong answer this anchor exists to remove.
863
+ */
864
+ private validatedBodyContractText;
813
865
  /**
814
866
  * Anchor (b): the contract declared in the route's validation schema.
815
867
  *
@@ -123,6 +123,16 @@ const RESPONSE_HELPER_MAX_DEPTH = 4;
123
123
  * the branch an error path, whose shape is not the endpoint's contract.
124
124
  */
125
125
  const STATUS_MEMBER_NAMES = ['status', 'statusCode'];
126
+ /**
127
+ * The parts of a request that ARE the body, as validator middleware names them
128
+ * (`validate('json', Schema)`). HTTP vocabulary for how a body is carried — the
129
+ * same class of generic route vocabulary as the `schema` / `body` / `response` /
130
+ * `handler` keys the schema anchors already read, and deliberately not a list of
131
+ * libraries. Every other part a validator can bind (`query`, `param`, `header`,
132
+ * `cookie`) is NOT the body, so a schema bound to one of those declares no
133
+ * request contract.
134
+ */
135
+ const REQUEST_BODY_PARTS = new Set(['json', 'form', 'body']);
126
136
  /**
127
137
  * Print a `Type` to its string form WITHOUT the compiler's default truncation.
128
138
  *
@@ -790,16 +800,15 @@ export class TypeInferrer {
790
800
  // ships as machinery and decays to `any` in the cross-repo surface.
791
801
  //
792
802
  // A DECLARED contract outranks whatever expression the locator picked,
793
- // exactly as it does on the response side, so read anchors (a) and (b)
794
- // first and fall through to the expression only when the route declares
795
- // its request nowhere. The third registration anchor — the typed request
796
- // READ inside the handler body — is deliberately not consulted here: that
797
- // one is itself an expression, so the locator's own expression stays
798
- // authoritative when nothing is declared.
803
+ // exactly as it does on the response side, so read the DECLARED anchors
804
+ // (a), (b) and (b2) first and fall through to the expression only when the
805
+ // route declares its request nowhere. The remaining registration anchor —
806
+ // the typed request READ inside the handler body — is deliberately not
807
+ // consulted here: that one is itself an expression, so the locator's own
808
+ // expression stays authoritative when nothing is declared.
799
809
  const declaredAt = this.registrationAtLine(sourceFile, request.line_number);
800
810
  if (declaredAt) {
801
- const declared = this.requestBodyFromHandlerParams(declaredAt.handler) ??
802
- this.routeSchemaContractText(declaredAt.registration, 'body');
811
+ const declared = this.declaredRequestContract(declaredAt.registration, declaredAt.handler);
803
812
  if (declared) {
804
813
  this.log(`Route registration at ${request.file_path}:${request.line_number} declares its ` +
805
814
  'request contract; using the declaration over the located expression');
@@ -895,6 +904,19 @@ export class TypeInferrer {
895
904
  typeString = explicitType;
896
905
  isExplicit = true;
897
906
  }
907
+ // Publication guard (carrick#964): the locator landed on machinery — a
908
+ // callable member of the handler's context, a method reference, a handler
909
+ // binding — and neither a wrapper rule nor a declared type recovered a
910
+ // payload from it. Publishing that would assert an explicit contract for a
911
+ // type nothing can send over the wire, so abstain and let the row say
912
+ // unknown.
913
+ if (this.isCallableType(payloadType) &&
914
+ !unwrapResult.wasUnwrapped &&
915
+ !explicitType) {
916
+ this.log(`Request locator at ${request.file_path}:${request.line_number} resolved a callable ` +
917
+ `(${typeString}); a function is not a payload, leaving unresolved`);
918
+ return null;
919
+ }
898
920
  typeString = this.unwrapPromise(typeString, payloadType);
899
921
  return this.createInferredType(request, typeString, isExplicit, this.getNodeLocation(node), unwrapResult.wasUnwrapped ? unwrapResult.typeString : undefined);
900
922
  }
@@ -1611,6 +1633,32 @@ export class TypeInferrer {
1611
1633
  const trimmed = typeString.trim();
1612
1634
  return useless.includes(trimmed) || trimmed === '';
1613
1635
  }
1636
+ /**
1637
+ * A type that can be CALLED or constructed is machinery, never a payload
1638
+ * (carrick#964).
1639
+ *
1640
+ * A context-object framework hands the handler one object that both reads the
1641
+ * request and sends the response, so that object has a `body` MEMBER whose
1642
+ * type is the response sender. Every anchor that reads a member off the
1643
+ * handler's parameter, and every locator that lands on such a member, can
1644
+ * therefore resolve to a perfectly concrete type that is the wrong side of
1645
+ * the exchange — and, being concrete, it sails past the useless-type guard and
1646
+ * publishes as an explicit contract.
1647
+ *
1648
+ * Nothing crosses an HTTP (or queue, or topic) boundary as a function, so the
1649
+ * presence of call or construct signatures is a structural, framework-free
1650
+ * proof that the resolution landed on machinery. The caller abstains, which
1651
+ * leaves the row honestly unresolved instead of confidently wrong.
1652
+ */
1653
+ isCallableType(type) {
1654
+ try {
1655
+ return (type.getCallSignatures().length > 0 ||
1656
+ type.getConstructSignatures().length > 0);
1657
+ }
1658
+ catch {
1659
+ return false;
1660
+ }
1661
+ }
1614
1662
  unwrapExpressionNode(node) {
1615
1663
  let current = node;
1616
1664
  while (current) {
@@ -2799,6 +2847,9 @@ export class TypeInferrer {
2799
2847
  structuralTextFromTypeNode(typeNode) {
2800
2848
  try {
2801
2849
  const resolved = this.unwrapPromiseType(typeNode.getType());
2850
+ if (this.isCallableType(resolved)) {
2851
+ return null;
2852
+ }
2802
2853
  const bare = typeText(resolved, typeNode);
2803
2854
  if (this.isUselessType(bare)) {
2804
2855
  return null;
@@ -2817,6 +2868,9 @@ export class TypeInferrer {
2817
2868
  structuralTextFromType(type, at) {
2818
2869
  try {
2819
2870
  const resolved = this.unwrapPromiseType(type);
2871
+ if (this.isCallableType(resolved)) {
2872
+ return null;
2873
+ }
2820
2874
  const bare = typeText(resolved, at);
2821
2875
  if (this.isUselessType(bare)) {
2822
2876
  return null;
@@ -2891,9 +2945,24 @@ export class TypeInferrer {
2891
2945
  * the route declares its request nowhere we can read.
2892
2946
  */
2893
2947
  requestContractFromRegistration(registration, handler) {
2948
+ return (this.declaredRequestContract(registration, handler) ??
2949
+ this.inferRequestReadFromHandler(handler));
2950
+ }
2951
+ /**
2952
+ * The request contract a route DECLARES, in anchor order: the handler's
2953
+ * parameter annotation (a), the registration's schema object (b), then a
2954
+ * validator middleware bound to the request body (b2).
2955
+ *
2956
+ * Both callers — the registration locator and the expression locator whose
2957
+ * line is a registration — consult exactly this set, so the two cannot drift.
2958
+ * The typed request READ inside the handler body is deliberately not part of
2959
+ * it: that anchor is itself an expression, and the expression path keeps its
2960
+ * own locator authoritative when a route declares nothing.
2961
+ */
2962
+ declaredRequestContract(registration, handler) {
2894
2963
  return (this.requestBodyFromHandlerParams(handler) ??
2895
2964
  this.routeSchemaContractText(registration, 'body') ??
2896
- this.inferRequestReadFromHandler(handler));
2965
+ this.validatedBodyContractText(registration));
2897
2966
  }
2898
2967
  /**
2899
2968
  * Anchor (a): the request contract declared on the handler's own signature.
@@ -2934,6 +3003,58 @@ export class TypeInferrer {
2934
3003
  }
2935
3004
  return null;
2936
3005
  }
3006
+ /**
3007
+ * Anchor (b2): the contract declared by a VALIDATOR MIDDLEWARE on the
3008
+ * registration (carrick#964).
3009
+ *
3010
+ * A route can declare its body by binding a schema to a named part of the
3011
+ * request in a middleware the registration carries alongside the handler:
3012
+ *
3013
+ * router.post('/search', validate('json', PayloadSchema), async (c) => …)
3014
+ *
3015
+ * The shape is a CALL among the registration's arguments whose own arguments
3016
+ * are a request part and a schema value. Neither the middleware's name nor
3017
+ * the schema library is checked: the part is read off the string literal, and
3018
+ * the schema is whatever exposes a parsed output (`schemaOutputTypeText`) —
3019
+ * the same test anchor (b) already applies to a `schema: { body: … }` object.
3020
+ *
3021
+ * `REQUEST_BODY_PARTS` is HTTP vocabulary for how a body is carried, not a
3022
+ * framework list. A middleware bound to any other part (`query`, `param`,
3023
+ * `header`, `cookie`) declares no BODY, and a middleware that names no part
3024
+ * at all is not read: publishing a query schema as the request contract would
3025
+ * be the same confident-and-wrong answer this anchor exists to remove.
3026
+ */
3027
+ validatedBodyContractText(registration) {
3028
+ if (!Node.isCallExpression(registration)) {
3029
+ return null;
3030
+ }
3031
+ for (const argument of registration.getArguments()) {
3032
+ const middleware = this.unwrapExpressionNode(argument);
3033
+ if (!Node.isCallExpression(middleware)) {
3034
+ continue;
3035
+ }
3036
+ const middlewareArgs = middleware
3037
+ .getArguments()
3038
+ .map((arg) => this.unwrapExpressionNode(arg));
3039
+ const parts = middlewareArgs.filter((arg) => Node.isStringLiteral(arg));
3040
+ if (parts.length === 0) {
3041
+ continue;
3042
+ }
3043
+ if (!parts.some((part) => REQUEST_BODY_PARTS.has(part.getLiteralValue().toLowerCase()))) {
3044
+ continue;
3045
+ }
3046
+ for (const candidate of middlewareArgs) {
3047
+ if (Node.isStringLiteral(candidate)) {
3048
+ continue;
3049
+ }
3050
+ const declared = this.schemaOutputTypeText(candidate);
3051
+ if (declared) {
3052
+ return declared;
3053
+ }
3054
+ }
3055
+ }
3056
+ return null;
3057
+ }
2937
3058
  /**
2938
3059
  * Anchor (b): the contract declared in the route's validation schema.
2939
3060
  *