#
tokens: 48600/50000 14/825 files (page 17/61)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 17 of 61. Use http://codebase.md/taurgis/sfcc-dev-mcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .DS_Store
├── .github
│   ├── dependabot.yml
│   ├── instructions
│   │   ├── mcp-node-tests.instructions.md
│   │   └── mcp-yml-tests.instructions.md
│   ├── ISSUE_TEMPLATE
│   │   ├── bug_report.yml
│   │   ├── config.yml
│   │   ├── documentation.yml
│   │   ├── feature_request.yml
│   │   └── question.yml
│   ├── PULL_REQUEST_TEMPLATE
│   │   ├── bug_fix.md
│   │   ├── documentation.md
│   │   └── new_tool.md
│   ├── pull_request_template.md
│   └── workflows
│       ├── ci.yml
│       ├── deploy-pages.yml
│       ├── publish.yml
│       └── update-docs.yml
├── .gitignore
├── .husky
│   └── pre-commit
├── aegis.config.docs-only.json
├── aegis.config.json
├── aegis.config.with-dw.json
├── AGENTS.md
├── ai-instructions
│   ├── claude-desktop
│   │   └── claude_custom_instructions.md
│   ├── cursor
│   │   └── .cursor
│   │       └── rules
│   │           ├── debugging-workflows.mdc
│   │           ├── hooks-development.mdc
│   │           ├── isml-templates.mdc
│   │           ├── job-framework.mdc
│   │           ├── performance-optimization.mdc
│   │           ├── scapi-endpoints.mdc
│   │           ├── security-patterns.mdc
│   │           ├── sfcc-development.mdc
│   │           ├── sfra-controllers.mdc
│   │           ├── sfra-models.mdc
│   │           ├── system-objects.mdc
│   │           └── testing-patterns.mdc
│   └── github-copilot
│       └── copilot-instructions.md
├── CHANGELOG.md
├── CONTRIBUTING.md
├── docs
│   ├── best-practices
│   │   ├── cartridge_creation.md
│   │   ├── isml_templates.md
│   │   ├── job_framework.md
│   │   ├── localserviceregistry.md
│   │   ├── ocapi_hooks.md
│   │   ├── performance.md
│   │   ├── scapi_custom_endpoint.md
│   │   ├── scapi_hooks.md
│   │   ├── security.md
│   │   ├── sfra_client_side_js.md
│   │   ├── sfra_controllers.md
│   │   ├── sfra_models.md
│   │   └── sfra_scss.md
│   ├── dw_campaign
│   │   ├── ABTest.md
│   │   ├── ABTestMgr.md
│   │   ├── ABTestSegment.md
│   │   ├── AmountDiscount.md
│   │   ├── ApproachingDiscount.md
│   │   ├── BonusChoiceDiscount.md
│   │   ├── BonusDiscount.md
│   │   ├── Campaign.md
│   │   ├── CampaignMgr.md
│   │   ├── CampaignStatusCodes.md
│   │   ├── Coupon.md
│   │   ├── CouponMgr.md
│   │   ├── CouponRedemption.md
│   │   ├── CouponStatusCodes.md
│   │   ├── Discount.md
│   │   ├── DiscountPlan.md
│   │   ├── FixedPriceDiscount.md
│   │   ├── FixedPriceShippingDiscount.md
│   │   ├── FreeDiscount.md
│   │   ├── FreeShippingDiscount.md
│   │   ├── PercentageDiscount.md
│   │   ├── PercentageOptionDiscount.md
│   │   ├── PriceBookPriceDiscount.md
│   │   ├── Promotion.md
│   │   ├── PromotionMgr.md
│   │   ├── PromotionPlan.md
│   │   ├── SlotContent.md
│   │   ├── SourceCodeGroup.md
│   │   ├── SourceCodeInfo.md
│   │   ├── SourceCodeStatusCodes.md
│   │   └── TotalFixedPriceDiscount.md
│   ├── dw_catalog
│   │   ├── Catalog.md
│   │   ├── CatalogMgr.md
│   │   ├── Category.md
│   │   ├── CategoryAssignment.md
│   │   ├── CategoryLink.md
│   │   ├── PriceBook.md
│   │   ├── PriceBookMgr.md
│   │   ├── Product.md
│   │   ├── ProductActiveData.md
│   │   ├── ProductAttributeModel.md
│   │   ├── ProductAvailabilityLevels.md
│   │   ├── ProductAvailabilityModel.md
│   │   ├── ProductInventoryList.md
│   │   ├── ProductInventoryMgr.md
│   │   ├── ProductInventoryRecord.md
│   │   ├── ProductLink.md
│   │   ├── ProductMgr.md
│   │   ├── ProductOption.md
│   │   ├── ProductOptionModel.md
│   │   ├── ProductOptionValue.md
│   │   ├── ProductPriceInfo.md
│   │   ├── ProductPriceModel.md
│   │   ├── ProductPriceTable.md
│   │   ├── ProductSearchHit.md
│   │   ├── ProductSearchModel.md
│   │   ├── ProductSearchRefinementDefinition.md
│   │   ├── ProductSearchRefinements.md
│   │   ├── ProductSearchRefinementValue.md
│   │   ├── ProductVariationAttribute.md
│   │   ├── ProductVariationAttributeValue.md
│   │   ├── ProductVariationModel.md
│   │   ├── Recommendation.md
│   │   ├── SearchModel.md
│   │   ├── SearchRefinementDefinition.md
│   │   ├── SearchRefinements.md
│   │   ├── SearchRefinementValue.md
│   │   ├── SortingOption.md
│   │   ├── SortingRule.md
│   │   ├── Store.md
│   │   ├── StoreGroup.md
│   │   ├── StoreInventoryFilter.md
│   │   ├── StoreInventoryFilterValue.md
│   │   ├── StoreMgr.md
│   │   ├── Variant.md
│   │   └── VariationGroup.md
│   ├── dw_content
│   │   ├── Content.md
│   │   ├── ContentMgr.md
│   │   ├── ContentSearchModel.md
│   │   ├── ContentSearchRefinementDefinition.md
│   │   ├── ContentSearchRefinements.md
│   │   ├── ContentSearchRefinementValue.md
│   │   ├── Folder.md
│   │   ├── Library.md
│   │   ├── MarkupText.md
│   │   └── MediaFile.md
│   ├── dw_crypto
│   │   ├── CertificateRef.md
│   │   ├── CertificateUtils.md
│   │   ├── Cipher.md
│   │   ├── Encoding.md
│   │   ├── JWE.md
│   │   ├── JWEHeader.md
│   │   ├── JWS.md
│   │   ├── JWSHeader.md
│   │   ├── KeyRef.md
│   │   ├── Mac.md
│   │   ├── MessageDigest.md
│   │   ├── SecureRandom.md
│   │   ├── Signature.md
│   │   ├── WeakCipher.md
│   │   ├── WeakMac.md
│   │   ├── WeakMessageDigest.md
│   │   ├── WeakSignature.md
│   │   └── X509Certificate.md
│   ├── dw_customer
│   │   ├── AddressBook.md
│   │   ├── AgentUserMgr.md
│   │   ├── AgentUserStatusCodes.md
│   │   ├── AuthenticationStatus.md
│   │   ├── Credentials.md
│   │   ├── Customer.md
│   │   ├── CustomerActiveData.md
│   │   ├── CustomerAddress.md
│   │   ├── CustomerCDPData.md
│   │   ├── CustomerContextMgr.md
│   │   ├── CustomerGroup.md
│   │   ├── CustomerList.md
│   │   ├── CustomerMgr.md
│   │   ├── CustomerPasswordConstraints.md
│   │   ├── CustomerPaymentInstrument.md
│   │   ├── CustomerStatusCodes.md
│   │   ├── EncryptedObject.md
│   │   ├── ExternalProfile.md
│   │   ├── OrderHistory.md
│   │   ├── ProductList.md
│   │   ├── ProductListItem.md
│   │   ├── ProductListItemPurchase.md
│   │   ├── ProductListMgr.md
│   │   ├── ProductListRegistrant.md
│   │   ├── Profile.md
│   │   └── Wallet.md
│   ├── dw_extensions.applepay
│   │   ├── ApplePayHookResult.md
│   │   └── ApplePayHooks.md
│   ├── dw_extensions.facebook
│   │   ├── FacebookFeedHooks.md
│   │   └── FacebookProduct.md
│   ├── dw_extensions.paymentrequest
│   │   ├── PaymentRequestHookResult.md
│   │   └── PaymentRequestHooks.md
│   ├── dw_extensions.payments
│   │   ├── SalesforceBancontactPaymentDetails.md
│   │   ├── SalesforceCardPaymentDetails.md
│   │   ├── SalesforceEpsPaymentDetails.md
│   │   ├── SalesforceIdealPaymentDetails.md
│   │   ├── SalesforceKlarnaPaymentDetails.md
│   │   ├── SalesforcePaymentDetails.md
│   │   ├── SalesforcePaymentIntent.md
│   │   ├── SalesforcePaymentMethod.md
│   │   ├── SalesforcePaymentRequest.md
│   │   ├── SalesforcePaymentsHooks.md
│   │   ├── SalesforcePaymentsMgr.md
│   │   ├── SalesforcePaymentsSiteConfiguration.md
│   │   ├── SalesforcePayPalOrder.md
│   │   ├── SalesforcePayPalOrderAddress.md
│   │   ├── SalesforcePayPalOrderPayer.md
│   │   ├── SalesforcePayPalPaymentDetails.md
│   │   ├── SalesforceSepaDebitPaymentDetails.md
│   │   └── SalesforceVenmoPaymentDetails.md
│   ├── dw_extensions.pinterest
│   │   ├── PinterestAvailability.md
│   │   ├── PinterestFeedHooks.md
│   │   ├── PinterestOrder.md
│   │   ├── PinterestOrderHooks.md
│   │   └── PinterestProduct.md
│   ├── dw_io
│   │   ├── CSVStreamReader.md
│   │   ├── CSVStreamWriter.md
│   │   ├── File.md
│   │   ├── FileReader.md
│   │   ├── FileWriter.md
│   │   ├── InputStream.md
│   │   ├── OutputStream.md
│   │   ├── PrintWriter.md
│   │   ├── RandomAccessFileReader.md
│   │   ├── Reader.md
│   │   ├── StringWriter.md
│   │   ├── Writer.md
│   │   ├── XMLIndentingStreamWriter.md
│   │   ├── XMLStreamConstants.md
│   │   ├── XMLStreamReader.md
│   │   └── XMLStreamWriter.md
│   ├── dw_job
│   │   ├── JobExecution.md
│   │   └── JobStepExecution.md
│   ├── dw_net
│   │   ├── FTPClient.md
│   │   ├── FTPFileInfo.md
│   │   ├── HTTPClient.md
│   │   ├── HTTPRequestPart.md
│   │   ├── Mail.md
│   │   ├── SFTPClient.md
│   │   ├── SFTPFileInfo.md
│   │   ├── WebDAVClient.md
│   │   └── WebDAVFileInfo.md
│   ├── dw_object
│   │   ├── ActiveData.md
│   │   ├── CustomAttributes.md
│   │   ├── CustomObject.md
│   │   ├── CustomObjectMgr.md
│   │   ├── Extensible.md
│   │   ├── ExtensibleObject.md
│   │   ├── Note.md
│   │   ├── ObjectAttributeDefinition.md
│   │   ├── ObjectAttributeGroup.md
│   │   ├── ObjectAttributeValueDefinition.md
│   │   ├── ObjectTypeDefinition.md
│   │   ├── PersistentObject.md
│   │   ├── SimpleExtensible.md
│   │   └── SystemObjectMgr.md
│   ├── dw_order
│   │   ├── AbstractItem.md
│   │   ├── AbstractItemCtnr.md
│   │   ├── Appeasement.md
│   │   ├── AppeasementItem.md
│   │   ├── Basket.md
│   │   ├── BasketMgr.md
│   │   ├── BonusDiscountLineItem.md
│   │   ├── CouponLineItem.md
│   │   ├── CreateAgentBasketLimitExceededException.md
│   │   ├── CreateBasketFromOrderException.md
│   │   ├── CreateCouponLineItemException.md
│   │   ├── CreateOrderException.md
│   │   ├── CreateTemporaryBasketLimitExceededException.md
│   │   ├── GiftCertificate.md
│   │   ├── GiftCertificateLineItem.md
│   │   ├── GiftCertificateMgr.md
│   │   ├── GiftCertificateStatusCodes.md
│   │   ├── Invoice.md
│   │   ├── InvoiceItem.md
│   │   ├── LineItem.md
│   │   ├── LineItemCtnr.md
│   │   ├── Order.md
│   │   ├── OrderAddress.md
│   │   ├── OrderItem.md
│   │   ├── OrderMgr.md
│   │   ├── OrderPaymentInstrument.md
│   │   ├── OrderProcessStatusCodes.md
│   │   ├── PaymentCard.md
│   │   ├── PaymentInstrument.md
│   │   ├── PaymentMethod.md
│   │   ├── PaymentMgr.md
│   │   ├── PaymentProcessor.md
│   │   ├── PaymentStatusCodes.md
│   │   ├── PaymentTransaction.md
│   │   ├── PriceAdjustment.md
│   │   ├── PriceAdjustmentLimitTypes.md
│   │   ├── ProductLineItem.md
│   │   ├── ProductShippingCost.md
│   │   ├── ProductShippingLineItem.md
│   │   ├── ProductShippingModel.md
│   │   ├── Return.md
│   │   ├── ReturnCase.md
│   │   ├── ReturnCaseItem.md
│   │   ├── ReturnItem.md
│   │   ├── Shipment.md
│   │   ├── ShipmentShippingCost.md
│   │   ├── ShipmentShippingModel.md
│   │   ├── ShippingLineItem.md
│   │   ├── ShippingLocation.md
│   │   ├── ShippingMethod.md
│   │   ├── ShippingMgr.md
│   │   ├── ShippingOrder.md
│   │   ├── ShippingOrderItem.md
│   │   ├── SumItem.md
│   │   ├── TaxGroup.md
│   │   ├── TaxItem.md
│   │   ├── TaxMgr.md
│   │   ├── TrackingInfo.md
│   │   └── TrackingRef.md
│   ├── dw_order.hooks
│   │   ├── CalculateHooks.md
│   │   ├── OrderHooks.md
│   │   ├── PaymentHooks.md
│   │   ├── ReturnHooks.md
│   │   └── ShippingOrderHooks.md
│   ├── dw_rpc
│   │   ├── SOAPUtil.md
│   │   ├── Stub.md
│   │   └── WebReference.md
│   ├── dw_suggest
│   │   ├── BrandSuggestions.md
│   │   ├── CategorySuggestions.md
│   │   ├── ContentSuggestions.md
│   │   ├── CustomSuggestions.md
│   │   ├── ProductSuggestions.md
│   │   ├── SearchPhraseSuggestions.md
│   │   ├── SuggestedCategory.md
│   │   ├── SuggestedContent.md
│   │   ├── SuggestedPhrase.md
│   │   ├── SuggestedProduct.md
│   │   ├── SuggestedTerm.md
│   │   ├── SuggestedTerms.md
│   │   ├── Suggestions.md
│   │   └── SuggestModel.md
│   ├── dw_svc
│   │   ├── FTPService.md
│   │   ├── FTPServiceDefinition.md
│   │   ├── HTTPFormService.md
│   │   ├── HTTPFormServiceDefinition.md
│   │   ├── HTTPService.md
│   │   ├── HTTPServiceDefinition.md
│   │   ├── LocalServiceRegistry.md
│   │   ├── Result.md
│   │   ├── Service.md
│   │   ├── ServiceCallback.md
│   │   ├── ServiceConfig.md
│   │   ├── ServiceCredential.md
│   │   ├── ServiceDefinition.md
│   │   ├── ServiceProfile.md
│   │   ├── ServiceRegistry.md
│   │   ├── SOAPService.md
│   │   └── SOAPServiceDefinition.md
│   ├── dw_system
│   │   ├── AgentUserStatusCodes.md
│   │   ├── Cache.md
│   │   ├── CacheMgr.md
│   │   ├── HookMgr.md
│   │   ├── InternalObject.md
│   │   ├── JobProcessMonitor.md
│   │   ├── Log.md
│   │   ├── Logger.md
│   │   ├── LogNDC.md
│   │   ├── OrganizationPreferences.md
│   │   ├── Pipeline.md
│   │   ├── PipelineDictionary.md
│   │   ├── RemoteInclude.md
│   │   ├── Request.md
│   │   ├── RequestHooks.md
│   │   ├── Response.md
│   │   ├── RESTErrorResponse.md
│   │   ├── RESTResponseMgr.md
│   │   ├── RESTSuccessResponse.md
│   │   ├── SearchStatus.md
│   │   ├── Session.md
│   │   ├── Site.md
│   │   ├── SitePreferences.md
│   │   ├── Status.md
│   │   ├── StatusItem.md
│   │   ├── System.md
│   │   └── Transaction.md
│   ├── dw_util
│   │   ├── ArrayList.md
│   │   ├── Assert.md
│   │   ├── BigInteger.md
│   │   ├── Bytes.md
│   │   ├── Calendar.md
│   │   ├── Collection.md
│   │   ├── Currency.md
│   │   ├── DateUtils.md
│   │   ├── Decimal.md
│   │   ├── FilteringCollection.md
│   │   ├── Geolocation.md
│   │   ├── HashMap.md
│   │   ├── HashSet.md
│   │   ├── Iterator.md
│   │   ├── LinkedHashMap.md
│   │   ├── LinkedHashSet.md
│   │   ├── List.md
│   │   ├── Locale.md
│   │   ├── Map.md
│   │   ├── MapEntry.md
│   │   ├── MappingKey.md
│   │   ├── MappingMgr.md
│   │   ├── PropertyComparator.md
│   │   ├── SecureEncoder.md
│   │   ├── SecureFilter.md
│   │   ├── SeekableIterator.md
│   │   ├── Set.md
│   │   ├── SortedMap.md
│   │   ├── SortedSet.md
│   │   ├── StringUtils.md
│   │   ├── Template.md
│   │   └── UUIDUtils.md
│   ├── dw_value
│   │   ├── EnumValue.md
│   │   ├── MimeEncodedText.md
│   │   ├── Money.md
│   │   └── Quantity.md
│   ├── dw_web
│   │   ├── ClickStream.md
│   │   ├── ClickStreamEntry.md
│   │   ├── Cookie.md
│   │   ├── Cookies.md
│   │   ├── CSRFProtection.md
│   │   ├── Form.md
│   │   ├── FormAction.md
│   │   ├── FormElement.md
│   │   ├── FormElementValidationResult.md
│   │   ├── FormField.md
│   │   ├── FormFieldOption.md
│   │   ├── FormFieldOptions.md
│   │   ├── FormGroup.md
│   │   ├── FormList.md
│   │   ├── FormListItem.md
│   │   ├── Forms.md
│   │   ├── HttpParameter.md
│   │   ├── HttpParameterMap.md
│   │   ├── LoopIterator.md
│   │   ├── PageMetaData.md
│   │   ├── PageMetaTag.md
│   │   ├── PagingModel.md
│   │   ├── Resource.md
│   │   ├── URL.md
│   │   ├── URLAction.md
│   │   ├── URLParameter.md
│   │   ├── URLRedirect.md
│   │   ├── URLRedirectMgr.md
│   │   └── URLUtils.md
│   ├── sfra
│   │   ├── account.md
│   │   ├── address.md
│   │   ├── billing.md
│   │   ├── cart.md
│   │   ├── categories.md
│   │   ├── content.md
│   │   ├── locale.md
│   │   ├── order.md
│   │   ├── payment.md
│   │   ├── price-default.md
│   │   ├── price-range.md
│   │   ├── price-tiered.md
│   │   ├── product-bundle.md
│   │   ├── product-full.md
│   │   ├── product-line-items.md
│   │   ├── product-search.md
│   │   ├── product-tile.md
│   │   ├── querystring.md
│   │   ├── render.md
│   │   ├── request.md
│   │   ├── response.md
│   │   ├── server.md
│   │   ├── shipping.md
│   │   ├── store.md
│   │   ├── stores.md
│   │   └── totals.md
│   └── TopLevel
│       ├── APIException.md
│       ├── arguments.md
│       ├── Array.md
│       ├── ArrayBuffer.md
│       ├── BigInt.md
│       ├── Boolean.md
│       ├── ConversionError.md
│       ├── DataView.md
│       ├── Date.md
│       ├── Error.md
│       ├── ES6Iterator.md
│       ├── EvalError.md
│       ├── Fault.md
│       ├── Float32Array.md
│       ├── Float64Array.md
│       ├── Function.md
│       ├── Generator.md
│       ├── global.md
│       ├── Int16Array.md
│       ├── Int32Array.md
│       ├── Int8Array.md
│       ├── InternalError.md
│       ├── IOError.md
│       ├── Iterable.md
│       ├── Iterator.md
│       ├── JSON.md
│       ├── Map.md
│       ├── Math.md
│       ├── Module.md
│       ├── Namespace.md
│       ├── Number.md
│       ├── Object.md
│       ├── QName.md
│       ├── RangeError.md
│       ├── ReferenceError.md
│       ├── RegExp.md
│       ├── Set.md
│       ├── StopIteration.md
│       ├── String.md
│       ├── Symbol.md
│       ├── SyntaxError.md
│       ├── SystemError.md
│       ├── TypeError.md
│       ├── Uint16Array.md
│       ├── Uint32Array.md
│       ├── Uint8Array.md
│       ├── Uint8ClampedArray.md
│       ├── URIError.md
│       ├── WeakMap.md
│       ├── WeakSet.md
│       ├── XML.md
│       ├── XMLList.md
│       └── XMLStreamError.md
├── docs-site
│   ├── .gitignore
│   ├── App.tsx
│   ├── components
│   │   ├── Badge.tsx
│   │   ├── BreadcrumbSchema.tsx
│   │   ├── CodeBlock.tsx
│   │   ├── Collapsible.tsx
│   │   ├── ConfigBuilder.tsx
│   │   ├── ConfigHero.tsx
│   │   ├── ConfigModeTabs.tsx
│   │   ├── icons.tsx
│   │   ├── Layout.tsx
│   │   ├── LightCodeContainer.tsx
│   │   ├── NewcomerCTA.tsx
│   │   ├── NextStepsStrip.tsx
│   │   ├── OnThisPage.tsx
│   │   ├── Search.tsx
│   │   ├── SEO.tsx
│   │   ├── Sidebar.tsx
│   │   ├── StructuredData.tsx
│   │   ├── ToolCard.tsx
│   │   ├── ToolFilters.tsx
│   │   ├── Typography.tsx
│   │   └── VersionBadge.tsx
│   ├── constants.tsx
│   ├── index.html
│   ├── main.tsx
│   ├── metadata.json
│   ├── package-lock.json
│   ├── package.json
│   ├── pages
│   │   ├── AIInterfacesPage.tsx
│   │   ├── ConfigurationPage.tsx
│   │   ├── DevelopmentPage.tsx
│   │   ├── ExamplesPage.tsx
│   │   ├── FeaturesPage.tsx
│   │   ├── HomePage.tsx
│   │   ├── SecurityPage.tsx
│   │   ├── ToolsPage.tsx
│   │   └── TroubleshootingPage.tsx
│   ├── postcss.config.js
│   ├── public
│   │   ├── .well-known
│   │   │   └── security.txt
│   │   ├── 404.html
│   │   ├── android-chrome-192x192.png
│   │   ├── android-chrome-512x512.png
│   │   ├── apple-touch-icon.png
│   │   ├── explain-product-pricing-methods-no-mcp.png
│   │   ├── explain-product-pricing-methods.png
│   │   ├── favicon-16x16.png
│   │   ├── favicon-32x32.png
│   │   ├── favicon.ico
│   │   ├── llms.txt
│   │   ├── robots.txt
│   │   ├── site.webmanifest
│   │   └── sitemap.xml
│   ├── README.md
│   ├── scripts
│   │   ├── generate-search-index.js
│   │   ├── generate-sitemap.js
│   │   └── search-dev.js
│   ├── src
│   │   └── styles
│   │       ├── input.css
│   │       └── prism-theme.css
│   ├── tailwind.config.js
│   ├── tsconfig.json
│   ├── types.ts
│   ├── utils
│   │   ├── search.ts
│   │   └── toolsData.ts
│   └── vite.config.ts
├── eslint.config.js
├── jest.config.js
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── scripts
│   └── convert-docs.js
├── SECURITY.md
├── server.json
├── src
│   ├── clients
│   │   ├── base
│   │   │   ├── http-client.ts
│   │   │   ├── oauth-token.ts
│   │   │   └── ocapi-auth-client.ts
│   │   ├── best-practices-client.ts
│   │   ├── cartridge-generation-client.ts
│   │   ├── docs
│   │   │   ├── class-content-parser.ts
│   │   │   ├── class-name-resolver.ts
│   │   │   ├── documentation-scanner.ts
│   │   │   ├── index.ts
│   │   │   └── referenced-types-extractor.ts
│   │   ├── docs-client.ts
│   │   ├── log-client.ts
│   │   ├── logs
│   │   │   ├── index.ts
│   │   │   ├── log-analyzer.ts
│   │   │   ├── log-client.ts
│   │   │   ├── log-constants.ts
│   │   │   ├── log-file-discovery.ts
│   │   │   ├── log-file-reader.ts
│   │   │   ├── log-formatter.ts
│   │   │   ├── log-processor.ts
│   │   │   ├── log-types.ts
│   │   │   └── webdav-client-manager.ts
│   │   ├── ocapi
│   │   │   ├── code-versions-client.ts
│   │   │   ├── site-preferences-client.ts
│   │   │   └── system-objects-client.ts
│   │   ├── ocapi-client.ts
│   │   └── sfra-client.ts
│   ├── config
│   │   ├── configuration-factory.ts
│   │   └── dw-json-loader.ts
│   ├── core
│   │   ├── handlers
│   │   │   ├── abstract-log-tool-handler.ts
│   │   │   ├── base-handler.ts
│   │   │   ├── best-practices-handler.ts
│   │   │   ├── cartridge-handler.ts
│   │   │   ├── client-factory.ts
│   │   │   ├── code-version-handler.ts
│   │   │   ├── docs-handler.ts
│   │   │   ├── job-log-handler.ts
│   │   │   ├── job-log-tool-config.ts
│   │   │   ├── log-handler.ts
│   │   │   ├── log-tool-config.ts
│   │   │   ├── sfra-handler.ts
│   │   │   ├── system-object-handler.ts
│   │   │   └── validation-helpers.ts
│   │   ├── server.ts
│   │   └── tool-definitions.ts
│   ├── index.ts
│   ├── main.ts
│   ├── services
│   │   ├── file-system-service.ts
│   │   ├── index.ts
│   │   └── path-service.ts
│   ├── tool-configs
│   │   ├── best-practices-tool-config.ts
│   │   ├── cartridge-tool-config.ts
│   │   ├── code-version-tool-config.ts
│   │   ├── docs-tool-config.ts
│   │   ├── job-log-tool-config.ts
│   │   ├── log-tool-config.ts
│   │   ├── sfra-tool-config.ts
│   │   └── system-object-tool-config.ts
│   ├── types
│   │   └── types.ts
│   └── utils
│       ├── cache.ts
│       ├── job-log-tool-config.ts
│       ├── job-log-utils.ts
│       ├── log-cache.ts
│       ├── log-tool-config.ts
│       ├── log-tool-constants.ts
│       ├── log-tool-utils.ts
│       ├── logger.ts
│       ├── ocapi-url-builder.ts
│       ├── path-resolver.ts
│       ├── query-builder.ts
│       ├── utils.ts
│       └── validator.ts
├── tests
│   ├── __mocks__
│   │   ├── docs-client.ts
│   │   ├── src
│   │   │   └── clients
│   │   │       └── base
│   │   │           └── http-client.js
│   │   └── webdav.js
│   ├── base-handler.test.ts
│   ├── base-http-client.test.ts
│   ├── best-practices-handler.test.ts
│   ├── cache.test.ts
│   ├── cartridge-handler.test.ts
│   ├── class-content-parser.test.ts
│   ├── class-name-resolver.test.ts
│   ├── client-factory.test.ts
│   ├── code-version-handler.test.ts
│   ├── code-versions-client.test.ts
│   ├── config.test.ts
│   ├── configuration-factory.test.ts
│   ├── docs-handler.test.ts
│   ├── documentation-scanner.test.ts
│   ├── file-system-service.test.ts
│   ├── job-log-handler.test.ts
│   ├── job-log-utils.test.ts
│   ├── log-client.test.ts
│   ├── log-handler.test.ts
│   ├── log-processor.test.ts
│   ├── logger.test.ts
│   ├── mcp
│   │   ├── AGENTS.md
│   │   ├── node
│   │   │   ├── activate-code-version-advanced.full-mode.programmatic.test.js
│   │   │   ├── code-versions.full-mode.programmatic.test.js
│   │   │   ├── generate-cartridge-structure.docs-only.programmatic.test.js
│   │   │   ├── get-available-best-practice-guides.docs-only.programmatic.test.js
│   │   │   ├── get-available-sfra-documents.programmatic.test.js
│   │   │   ├── get-best-practice-guide.docs-only.programmatic.test.js
│   │   │   ├── get-hook-reference.docs-only.programmatic.test.js
│   │   │   ├── get-job-execution-summary.full-mode.programmatic.test.js
│   │   │   ├── get-job-log-entries.full-mode.programmatic.test.js
│   │   │   ├── get-latest-debug.full-mode.programmatic.test.js
│   │   │   ├── get-latest-error.full-mode.programmatic.test.js
│   │   │   ├── get-latest-info.full-mode.programmatic.test.js
│   │   │   ├── get-latest-job-log-files.full-mode.programmatic.test.js
│   │   │   ├── get-latest-warn.full-mode.programmatic.test.js
│   │   │   ├── get-log-file-contents.full-mode.programmatic.test.js
│   │   │   ├── get-sfcc-class-documentation.docs-only.programmatic.test.js
│   │   │   ├── get-sfcc-class-info.docs-only.programmatic.test.js
│   │   │   ├── get-sfra-categories.docs-only.programmatic.test.js
│   │   │   ├── get-sfra-document.programmatic.test.js
│   │   │   ├── get-sfra-documents-by-category.docs-only.programmatic.test.js
│   │   │   ├── get-system-object-definition.full-mode.programmatic.test.js
│   │   │   ├── get-system-object-definitions.docs-only.programmatic.test.js
│   │   │   ├── get-system-object-definitions.full-mode.programmatic.test.js
│   │   │   ├── list-log-files.full-mode.programmatic.test.js
│   │   │   ├── list-sfcc-classes.docs-only.programmatic.test.js
│   │   │   ├── search-best-practices.docs-only.programmatic.test.js
│   │   │   ├── search-custom-object-attribute-definitions.full-mode.programmatic.test.js
│   │   │   ├── search-job-logs-by-name.full-mode.programmatic.test.js
│   │   │   ├── search-job-logs.full-mode.programmatic.test.js
│   │   │   ├── search-logs.full-mode.programmatic.test.js
│   │   │   ├── search-sfcc-classes.docs-only.programmatic.test.js
│   │   │   ├── search-sfcc-methods.docs-only.programmatic.test.js
│   │   │   ├── search-sfra-documentation.docs-only.programmatic.test.js
│   │   │   ├── search-site-preferences.full-mode.programmatic.test.js
│   │   │   ├── search-system-object-attribute-definitions.full-mode.programmatic.test.js
│   │   │   ├── search-system-object-attribute-groups.full-mode.programmatic.test.js
│   │   │   ├── summarize-logs.full-mode.programmatic.test.js
│   │   │   ├── tools.docs-only.programmatic.test.js
│   │   │   └── tools.full-mode.programmatic.test.js
│   │   ├── README.md
│   │   ├── test-fixtures
│   │   │   └── dw.json
│   │   └── yaml
│   │       ├── activate-code-version.docs-only.test.mcp.yml
│   │       ├── activate-code-version.full-mode.test.mcp.yml
│   │       ├── get_latest_error.test.mcp.yml
│   │       ├── get-available-best-practice-guides.docs-only.test.mcp.yml
│   │       ├── get-available-best-practice-guides.full-mode.test.mcp.yml
│   │       ├── get-available-sfra-documents.docs-only.test.mcp.yml
│   │       ├── get-available-sfra-documents.full-mode.test.mcp.yml
│   │       ├── get-best-practice-guide.docs-only.test.mcp.yml
│   │       ├── get-best-practice-guide.full-mode.test.mcp.yml
│   │       ├── get-code-versions.docs-only.test.mcp.yml
│   │       ├── get-code-versions.full-mode.test.mcp.yml
│   │       ├── get-hook-reference.docs-only.test.mcp.yml
│   │       ├── get-hook-reference.full-mode.test.mcp.yml
│   │       ├── get-job-execution-summary.full-mode.test.mcp.yml
│   │       ├── get-job-log-entries.full-mode.test.mcp.yml
│   │       ├── get-latest-debug.full-mode.test.mcp.yml
│   │       ├── get-latest-error.full-mode.test.mcp.yml
│   │       ├── get-latest-info.full-mode.test.mcp.yml
│   │       ├── get-latest-job-log-files.full-mode.test.mcp.yml
│   │       ├── get-latest-warn.full-mode.test.mcp.yml
│   │       ├── get-log-file-contents.full-mode.test.mcp.yml
│   │       ├── get-sfcc-class-documentation.docs-only.test.mcp.yml
│   │       ├── get-sfcc-class-documentation.full-mode.test.mcp.yml
│   │       ├── get-sfcc-class-info.docs-only.test.mcp.yml
│   │       ├── get-sfcc-class-info.full-mode.test.mcp.yml
│   │       ├── get-sfra-categories.docs-only.test.mcp.yml
│   │       ├── get-sfra-categories.full-mode.test.mcp.yml
│   │       ├── get-sfra-document.docs-only.test.mcp.yml
│   │       ├── get-sfra-document.full-mode.test.mcp.yml
│   │       ├── get-sfra-documents-by-category.docs-only.test.mcp.yml
│   │       ├── get-sfra-documents-by-category.full-mode.test.mcp.yml
│   │       ├── get-system-object-definition.docs-only.test.mcp.yml
│   │       ├── get-system-object-definition.full-mode.test.mcp.yml
│   │       ├── get-system-object-definitions.docs-only.test.mcp.yml
│   │       ├── get-system-object-definitions.full-mode.test.mcp.yml
│   │       ├── list-log-files.full-mode.test.mcp.yml
│   │       ├── list-sfcc-classes.docs-only.test.mcp.yml
│   │       ├── list-sfcc-classes.full-mode.test.mcp.yml
│   │       ├── search-best-practices.docs-only.test.mcp.yml
│   │       ├── search-best-practices.full-mode.test.mcp.yml
│   │       ├── search-custom-object-attribute-definitions.docs-only.test.mcp.yml
│   │       ├── search-custom-object-attribute-definitions.test.mcp.yml
│   │       ├── search-job-logs-by-name.full-mode.test.mcp.yml
│   │       ├── search-job-logs.full-mode.test.mcp.yml
│   │       ├── search-logs.full-mode.test.mcp.yml
│   │       ├── search-sfcc-classes.docs-only.test.mcp.yml
│   │       ├── search-sfcc-classes.full-mode.test.mcp.yml
│   │       ├── search-sfcc-methods.docs-only.test.mcp.yml
│   │       ├── search-sfcc-methods.full-mode.test.mcp.yml
│   │       ├── search-sfra-documentation.docs-only.test.mcp.yml
│   │       ├── search-sfra-documentation.full-mode.test.mcp.yml
│   │       ├── search-site-preferences.docs-only.test.mcp.yml
│   │       ├── search-site-preferences.full-mode.test.mcp.yml
│   │       ├── search-system-object-attribute-definitions.docs-only.test.mcp.yml
│   │       ├── search-system-object-attribute-definitions.full-mode.test.mcp.yml
│   │       ├── search-system-object-attribute-groups.docs-only.test.mcp.yml
│   │       ├── search-system-object-attribute-groups.full-mode.test.mcp.yml
│   │       ├── summarize-logs.full-mode.test.mcp.yml
│   │       ├── tools.docs-only.test.mcp.yml
│   │       └── tools.full-mode.test.mcp.yml
│   ├── oauth-token.test.ts
│   ├── ocapi-auth-client.test.ts
│   ├── ocapi-client.test.ts
│   ├── path-service.test.ts
│   ├── query-builder.test.ts
│   ├── referenced-types-extractor.test.ts
│   ├── servers
│   │   ├── sfcc-mock-server
│   │   │   ├── mock-data
│   │   │   │   └── ocapi
│   │   │   │       ├── code-versions.json
│   │   │   │       ├── custom-object-attributes-customapi.json
│   │   │   │       ├── custom-object-attributes-globalsettings.json
│   │   │   │       ├── custom-object-attributes-versionhistory.json
│   │   │   │       ├── site-preferences-ccv.json
│   │   │   │       ├── site-preferences-fastforward.json
│   │   │   │       ├── site-preferences-sfra.json
│   │   │   │       ├── site-preferences-storefront.json
│   │   │   │       ├── site-preferences-system.json
│   │   │   │       ├── system-object-attribute-groups-campaign.json
│   │   │   │       ├── system-object-attribute-groups-category.json
│   │   │   │       ├── system-object-attribute-groups-order.json
│   │   │   │       ├── system-object-attribute-groups-product.json
│   │   │   │       ├── system-object-attribute-groups-sitepreferences.json
│   │   │   │       ├── system-object-attributes-customeraddress.json
│   │   │   │       ├── system-object-attributes-product-expanded.json
│   │   │   │       ├── system-object-attributes-product.json
│   │   │   │       ├── system-object-definition-category.json
│   │   │   │       ├── system-object-definition-customer.json
│   │   │   │       ├── system-object-definition-customeraddress.json
│   │   │   │       ├── system-object-definition-order.json
│   │   │   │       ├── system-object-definition-product.json
│   │   │   │       ├── system-object-definitions-old.json
│   │   │   │       └── system-object-definitions.json
│   │   │   ├── package-lock.json
│   │   │   ├── package.json
│   │   │   ├── README.md
│   │   │   ├── scripts
│   │   │   │   └── setup-logs.js
│   │   │   ├── server.js
│   │   │   └── src
│   │   │       ├── app.js
│   │   │       ├── config
│   │   │       │   └── server-config.js
│   │   │       ├── middleware
│   │   │       │   ├── auth.js
│   │   │       │   ├── cors.js
│   │   │       │   └── logging.js
│   │   │       ├── routes
│   │   │       │   ├── ocapi
│   │   │       │   │   ├── code-versions-handler.js
│   │   │       │   │   ├── oauth-handler.js
│   │   │       │   │   ├── ocapi-error-utils.js
│   │   │       │   │   ├── ocapi-utils.js
│   │   │       │   │   ├── site-preferences-handler.js
│   │   │       │   │   └── system-objects-handler.js
│   │   │       │   ├── ocapi.js
│   │   │       │   └── webdav.js
│   │   │       └── utils
│   │   │           ├── mock-data-loader.js
│   │   │           └── webdav-xml.js
│   │   └── sfcc-mock-server-manager.ts
│   ├── sfcc-mock-server.test.ts
│   ├── site-preferences-client.test.ts
│   ├── system-objects-client.test.ts
│   ├── utils.test.ts
│   ├── validation-helpers.test.ts
│   └── validator.test.ts
├── tsconfig.json
└── tsconfig.test.json
```

# Files

--------------------------------------------------------------------------------
/src/clients/logs/log-analyzer.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Log analysis, summarization, and pattern detection
  3 |  */
  4 | 
  5 | import { Logger } from '../../utils/logger.js';
  6 | import { LogProcessor } from './log-processor.js';
  7 | import { LogFormatter } from './log-formatter.js';
  8 | import type { LogSummary, LogFileMetadata, ProcessedLogEntry } from './log-types.js';
  9 | 
 10 | export class LogAnalyzer {
 11 |   private logger: Logger;
 12 |   private processor: LogProcessor;
 13 | 
 14 |   constructor(logger: Logger) {
 15 |     this.logger = logger;
 16 |     this.processor = new LogProcessor(logger);
 17 |   }
 18 | 
 19 |   /**
 20 |    * Analyze log files and generate comprehensive summary
 21 |    */
 22 |   async analyzeLogs(
 23 |     files: LogFileMetadata[],
 24 |     fileContents: Map<string, string>,
 25 |     date: string,
 26 |   ): Promise<LogSummary> {
 27 |     const summary: LogSummary = {
 28 |       date,
 29 |       errorCount: 0,
 30 |       warningCount: 0,
 31 |       infoCount: 0,
 32 |       debugCount: 0,
 33 |       keyIssues: [],
 34 |       files: files.map((f: LogFileMetadata) => f.filename),
 35 |     };
 36 | 
 37 |     // Analyze each log file for counts and patterns
 38 |     for (const file of files) {
 39 |       const content = fileContents.get(file.filename);
 40 |       if (!content) {
 41 |         this.logger.warn(`No content found for analysis: ${file.filename}`);
 42 |         continue;
 43 |       }
 44 | 
 45 |       try {
 46 |         // Count different log levels
 47 |         const counts = this.processor.countLogLevels(content);
 48 |         summary.errorCount += counts.errorCount;
 49 |         summary.warningCount += counts.warningCount;
 50 |         summary.infoCount += counts.infoCount;
 51 |         summary.debugCount += counts.debugCount;
 52 | 
 53 |         // Extract key issues from error files
 54 |         if (this.isErrorFile(file.filename)) {
 55 |           const issues = this.processor.extractKeyIssues(content);
 56 |           summary.keyIssues.push(...issues);
 57 |         }
 58 |       } catch (error) {
 59 |         this.logger.error(`Error analyzing file ${file.filename}:`, error);
 60 |       }
 61 |     }
 62 | 
 63 |     // Remove duplicate key issues
 64 |     summary.keyIssues = [...new Set(summary.keyIssues)];
 65 | 
 66 |     return summary;
 67 |   }
 68 | 
 69 |   /**
 70 |    * Detect patterns and anomalies in logs
 71 |    */
 72 |   detectPatterns(entries: ProcessedLogEntry[]): {
 73 |     frequentErrors: Map<string, number>;
 74 |     timePatterns: Map<string, number>;
 75 |     sourcePatterns: Map<string, number>;
 76 |   } {
 77 |     const frequentErrors = new Map<string, number>();
 78 |     const timePatterns = new Map<string, number>();
 79 |     const sourcePatterns = new Map<string, number>();
 80 | 
 81 |     for (const entry of entries) {
 82 |       // Count error patterns
 83 |       if (entry.level === 'error') {
 84 |         const errorPattern = this.extractErrorPattern(entry.content);
 85 |         frequentErrors.set(errorPattern, (frequentErrors.get(errorPattern) ?? 0) + 1);
 86 |       }
 87 | 
 88 |       // Count time patterns (hour-based)
 89 |       if (entry.timestamp) {
 90 |         const hour = new Date(entry.timestamp).getHours();
 91 |         const hourKey = `${hour}:00-${hour + 1}:00`;
 92 |         timePatterns.set(hourKey, (timePatterns.get(hourKey) ?? 0) + 1);
 93 |       }
 94 | 
 95 |       // Count source patterns
 96 |       if (entry.source) {
 97 |         sourcePatterns.set(entry.source, (sourcePatterns.get(entry.source) ?? 0) + 1);
 98 |       }
 99 |     }
100 | 
101 |     return {
102 |       frequentErrors,
103 |       timePatterns,
104 |       sourcePatterns,
105 |     };
106 |   }
107 | 
108 |   /**
109 |    * Generate health score based on log analysis
110 |    */
111 |   calculateHealthScore(summary: LogSummary): {
112 |     score: number;
113 |     level: 'excellent' | 'good' | 'warning' | 'critical';
114 |     factors: string[];
115 |   } {
116 |     const factors: string[] = [];
117 |     let score = 100;
118 | 
119 |     // Deduct points for errors
120 |     if (summary.errorCount > 0) {
121 |       const errorPenalty = Math.min(summary.errorCount * 2, 30);
122 |       score -= errorPenalty;
123 |       factors.push(`Errors detected: -${errorPenalty} points`);
124 |     }
125 | 
126 |     // Deduct points for warnings
127 |     if (summary.warningCount > 10) {
128 |       const warningPenalty = Math.min((summary.warningCount - 10) * 0.5, 15);
129 |       score -= warningPenalty;
130 |       factors.push(`High warning count: -${warningPenalty} points`);
131 |     }
132 | 
133 |     // Deduct points for key issues
134 |     if (summary.keyIssues.length > 0) {
135 |       const issuePenalty = Math.min(summary.keyIssues.length * 5, 25);
136 |       score -= issuePenalty;
137 |       factors.push(`Key issues: -${issuePenalty} points`);
138 |     }
139 | 
140 |     // Determine level
141 |     let level: 'excellent' | 'good' | 'warning' | 'critical';
142 |     if (score >= 90) {
143 |       level = 'excellent';
144 |     } else if (score >= 75) {
145 |       level = 'good';
146 |     } else if (score >= 50) {
147 |       level = 'warning';
148 |     } else {
149 |       level = 'critical';
150 |     }
151 | 
152 |     return { score: Math.max(0, score), level, factors };
153 |   }
154 | 
155 |   /**
156 |    * Find trending issues across time periods
157 |    */
158 |   findTrendingIssues(
159 |     currentSummary: LogSummary,
160 |     previousSummaries: LogSummary[],
161 |   ): {
162 |     increasing: string[];
163 |     decreasing: string[];
164 |     new: string[];
165 |   } {
166 |     const trending = {
167 |       increasing: [] as string[],
168 |       decreasing: [] as string[],
169 |       new: [] as string[],
170 |     };
171 | 
172 |     // Compare current issues with previous periods
173 |     const previousIssues = new Set(
174 |       previousSummaries.flatMap(summary => summary.keyIssues),
175 |     );
176 | 
177 |     for (const issue of currentSummary.keyIssues) {
178 |       if (!previousIssues.has(issue)) {
179 |         trending.new.push(issue);
180 |       }
181 |     }
182 | 
183 |     // For increasing/decreasing, we'd need more sophisticated tracking
184 |     // This is a simplified version
185 |     const currentErrorCount = currentSummary.errorCount;
186 |     const avgPreviousErrors = previousSummaries.length > 0
187 |       ? previousSummaries.reduce((sum, s) => sum + s.errorCount, 0) / previousSummaries.length
188 |       : 0;
189 | 
190 |     if (currentErrorCount > avgPreviousErrors * 1.5) {
191 |       trending.increasing.push('Overall error rate');
192 |     } else if (currentErrorCount < avgPreviousErrors * 0.5) {
193 |       trending.decreasing.push('Overall error rate');
194 |     }
195 | 
196 |     return trending;
197 |   }
198 | 
199 |   /**
200 |    * Generate recommendations based on analysis
201 |    */
202 |   generateRecommendations(summary: LogSummary, patterns: ReturnType<typeof this.detectPatterns>): string[] {
203 |     const recommendations: string[] = [];
204 | 
205 |     if (summary.errorCount > 10) {
206 |       recommendations.push('High error count detected. Review error logs for critical issues.');
207 |     }
208 | 
209 |     if (summary.warningCount > 50) {
210 |       recommendations.push('High warning count. Consider addressing warnings to prevent future errors.');
211 |     }
212 | 
213 |     if (patterns.frequentErrors.size > 0) {
214 |       const topError = Array.from(patterns.frequentErrors.entries())
215 |         .sort((a, b) => b[1] - a[1])[0];
216 |       recommendations.push(`Most frequent error: "${topError[0]}" (${topError[1]} occurrences)`);
217 |     }
218 | 
219 |     if (patterns.timePatterns.size > 0) {
220 |       const peakHour = Array.from(patterns.timePatterns.entries())
221 |         .sort((a, b) => b[1] - a[1])[0];
222 |       recommendations.push(`Peak activity time: ${peakHour[0]} (${peakHour[1]} events)`);
223 |     }
224 | 
225 |     if (summary.keyIssues.length === 0 && summary.errorCount === 0) {
226 |       recommendations.push('System appears to be running smoothly with no critical issues detected.');
227 |     }
228 | 
229 |     return recommendations;
230 |   }
231 | 
232 |   /**
233 |    * Extract error pattern for categorization
234 |    */
235 |   private extractErrorPattern(errorContent: string): string {
236 |     // Extract the core error message, removing dynamic parts
237 |     const patterns = [
238 |       /Exception: (.+?)(\s+at\s|$)/,
239 |       /Error: (.+?)(\s+at\s|$)/,
240 |       /Failed to (.+?)(\s+\(|$)/,
241 |       /Cannot (.+?)(\s+\(|$)/,
242 |     ];
243 | 
244 |     for (const pattern of patterns) {
245 |       const match = errorContent.match(pattern);
246 |       if (match) {
247 |         return match[1].trim();
248 |       }
249 |     }
250 | 
251 |     // Fallback: use first 50 characters
252 |     return LogFormatter.truncateText(errorContent, 50);
253 |   }
254 | 
255 |   /**
256 |    * Check if filename indicates an error log file
257 |    */
258 |   private isErrorFile(filename: string): boolean {
259 |     const normalizedName = filename.toLowerCase();
260 |     return normalizedName.includes('error-') || normalizedName.includes('customerror-');
261 |   }
262 | 
263 |   /**
264 |    * Format analysis results for display
265 |    */
266 |   formatAnalysisResults(
267 |     summary: LogSummary,
268 |     patterns: ReturnType<typeof this.detectPatterns>,
269 |     healthScore: ReturnType<typeof this.calculateHealthScore>,
270 |     recommendations: string[],
271 |   ): string {
272 |     const sections = [
273 |       LogFormatter.formatLogSummary(summary),
274 |       '',
275 |       `🏥 Health Score: ${healthScore.score}/100 (${healthScore.level})`,
276 |       healthScore.factors.length > 0 ? `Factors: ${healthScore.factors.join(', ')}` : '',
277 |       '',
278 |       '🔍 Pattern Analysis:',
279 |       `- Unique error patterns: ${patterns.frequentErrors.size}`,
280 |       `- Active time periods: ${patterns.timePatterns.size}`,
281 |       `- Different sources: ${patterns.sourcePatterns.size}`,
282 |       '',
283 |       '💡 Recommendations:',
284 |       ...recommendations.map(rec => `- ${rec}`),
285 |     ].filter(Boolean);
286 | 
287 |     return sections.join('\n');
288 |   }
289 | }
290 | 
```

--------------------------------------------------------------------------------
/tests/best-practices-handler.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { BestPracticesToolHandler } from '../src/core/handlers/best-practices-handler.js';
  2 | import { HandlerContext } from '../src/core/handlers/base-handler.js';
  3 | import { Logger } from '../src/utils/logger.js';
  4 | 
  5 | // Mock the SFCCBestPracticesClient
  6 | const mockBestPracticesClient = {
  7 |   getAvailableGuides: jest.fn(),
  8 |   getBestPracticeGuide: jest.fn(),
  9 |   searchBestPractices: jest.fn(),
 10 |   getHookReference: jest.fn(),
 11 | };
 12 | 
 13 | jest.mock('../src/clients/best-practices-client.js', () => ({
 14 |   SFCCBestPracticesClient: jest.fn(() => mockBestPracticesClient),
 15 | }));
 16 | 
 17 | describe('BestPracticesToolHandler', () => {
 18 |   let mockLogger: jest.Mocked<Logger>;
 19 |   let mockClient: typeof mockBestPracticesClient;
 20 |   let context: HandlerContext;
 21 |   let handler: BestPracticesToolHandler;
 22 | 
 23 |   beforeEach(() => {
 24 |     mockLogger = {
 25 |       debug: jest.fn(),
 26 |       log: jest.fn(),
 27 |       error: jest.fn(),
 28 |       timing: jest.fn(),
 29 |       methodEntry: jest.fn(),
 30 |       methodExit: jest.fn(),
 31 |     } as any;
 32 | 
 33 |     // Reset mocks
 34 |     jest.clearAllMocks();
 35 | 
 36 |     // Use the mock client directly
 37 |     mockClient = mockBestPracticesClient;
 38 | 
 39 |     jest.spyOn(Logger, 'getChildLogger').mockReturnValue(mockLogger);
 40 | 
 41 |     context = {
 42 |       logger: mockLogger,
 43 |       config: null as any,
 44 |       capabilities: { canAccessLogs: false, canAccessOCAPI: false },
 45 |     };
 46 | 
 47 |     handler = new BestPracticesToolHandler(context, 'BestPractices');
 48 |   });
 49 | 
 50 |   afterEach(() => {
 51 |     jest.restoreAllMocks();
 52 |   });
 53 | 
 54 |   // Helper function to initialize handler for tests that need it
 55 |   const initializeHandler = async () => {
 56 |     await (handler as any).initialize();
 57 |   };
 58 | 
 59 |   describe('canHandle', () => {
 60 |     it('should handle best practices tools', () => {
 61 |       expect(handler.canHandle('get_available_best_practice_guides')).toBe(true);
 62 |       expect(handler.canHandle('get_best_practice_guide')).toBe(true);
 63 |       expect(handler.canHandle('search_best_practices')).toBe(true);
 64 |       expect(handler.canHandle('get_hook_reference')).toBe(true);
 65 |     });
 66 | 
 67 |     it('should not handle non-best-practices tools', () => {
 68 |       expect(handler.canHandle('get_latest_error')).toBe(false);
 69 |       expect(handler.canHandle('unknown_tool')).toBe(false);
 70 |     });
 71 |   });
 72 | 
 73 |   describe('initialization', () => {
 74 |     it('should initialize best practices client', async () => {
 75 |       await initializeHandler();
 76 | 
 77 |       const MockedConstructor = jest.requireMock('../src/clients/best-practices-client.js').SFCCBestPracticesClient;
 78 |       expect(MockedConstructor).toHaveBeenCalled();
 79 |       expect(mockLogger.debug).toHaveBeenCalledWith('Best practices client initialized');
 80 |     });
 81 |   });
 82 | 
 83 |   describe('disposal', () => {
 84 |     it('should dispose best practices client properly', async () => {
 85 |       await initializeHandler();
 86 |       await (handler as any).dispose();
 87 | 
 88 |       expect(mockLogger.debug).toHaveBeenCalledWith('Best practices client disposed');
 89 |     });
 90 |   });
 91 | 
 92 |   describe('get_available_best_practice_guides tool', () => {
 93 |     beforeEach(async () => {
 94 |       await initializeHandler();
 95 |       mockClient.getAvailableGuides.mockResolvedValue([
 96 |         { name: 'cartridge_creation', title: 'Cartridge Creation', description: 'Best practices for cartridge creation' },
 97 |         { name: 'isml_templates', title: 'ISML Templates', description: 'Best practices for ISML templates' },
 98 |       ]);
 99 |     });
100 | 
101 |     it('should handle get_available_best_practice_guides', async () => {
102 |       const result = await handler.handle('get_available_best_practice_guides', {}, Date.now());
103 | 
104 |       expect(mockClient.getAvailableGuides).toHaveBeenCalled();
105 |       expect(result.content[0].text).toContain('cartridge_creation');
106 |       expect(result.content[0].text).toContain('isml_templates');
107 |     });
108 |   });
109 | 
110 |   describe('get_best_practice_guide tool', () => {
111 |     beforeEach(async () => {
112 |       await initializeHandler();
113 |       mockClient.getBestPracticeGuide.mockResolvedValue({
114 |         title: 'Cartridge Creation Best Practices',
115 |         description: 'Complete guide for creating custom cartridges',
116 |         sections: ['Overview', 'Setup', 'Configuration'],
117 |         content: 'Detailed content about cartridge creation...',
118 |       });
119 |     });
120 | 
121 |     it('should handle get_best_practice_guide with guideName', async () => {
122 |       const args = { guideName: 'cartridge_creation' };
123 |       const result = await handler.handle('get_best_practice_guide', args, Date.now());
124 | 
125 |       expect(mockClient.getBestPracticeGuide).toHaveBeenCalledWith('cartridge_creation');
126 |       expect(result.content[0].text).toContain('Cartridge Creation Best Practices');
127 |     });
128 | 
129 |     it('should throw error when guideName is missing', async () => {
130 |       const result = await handler.handle('get_best_practice_guide', {}, Date.now());
131 |       expect(result.isError).toBe(true);
132 |       expect(result.content[0].text).toContain('guideName must be a non-empty string');
133 |     });
134 |   });
135 | 
136 |   describe('search_best_practices tool', () => {
137 |     beforeEach(async () => {
138 |       await initializeHandler();
139 |       mockClient.searchBestPractices.mockResolvedValue([
140 |         { guide: 'cartridge_creation', section: 'Setup', content: 'Cartridge setup instructions...' },
141 |         { guide: 'security', section: 'Authentication', content: 'Security best practices...' },
142 |       ]);
143 |     });
144 | 
145 |     it('should handle search_best_practices with query', async () => {
146 |       const args = { query: 'validation' };
147 |       const result = await handler.handle('search_best_practices', args, Date.now());
148 | 
149 |       expect(mockClient.searchBestPractices).toHaveBeenCalledWith('validation');
150 |       expect(result.content[0].text).toContain('cartridge_creation');
151 |       expect(result.content[0].text).toContain('security');
152 |     });
153 | 
154 |     it('should throw error when query is missing', async () => {
155 |       const result = await handler.handle('search_best_practices', {}, Date.now());
156 |       expect(result.isError).toBe(true);
157 |       expect(result.content[0].text).toContain('query must be a non-empty string');
158 |     });
159 | 
160 |     it('should throw error when query is empty', async () => {
161 |       const result = await handler.handle('search_best_practices', { query: '' }, Date.now());
162 |       expect(result.isError).toBe(true);
163 |       expect(result.content[0].text).toContain('query must be a non-empty string');
164 |     });
165 |   });
166 | 
167 |   describe('get_hook_reference tool', () => {
168 |     beforeEach(async () => {
169 |       await initializeHandler();
170 |       mockClient.getHookReference.mockResolvedValue({
171 |         type: 'OCAPI Hooks',
172 |         hooks: [
173 |           { endpoint: 'customers.post', description: 'Customer creation hook' },
174 |           { endpoint: 'orders.get', description: 'Order retrieval hook' },
175 |         ],
176 |       });
177 |     });
178 | 
179 |     it('should handle get_hook_reference with guideName', async () => {
180 |       const args = { guideName: 'ocapi_hooks' };
181 |       const result = await handler.handle('get_hook_reference', args, Date.now());
182 | 
183 |       expect(mockClient.getHookReference).toHaveBeenCalledWith('ocapi_hooks');
184 |       expect(result.content[0].text).toContain('OCAPI Hooks');
185 |       expect(result.content[0].text).toContain('customers.post');
186 |     });
187 | 
188 |     it('should throw error when guideName is missing', async () => {
189 |       const result = await handler.handle('get_hook_reference', {}, Date.now());
190 |       expect(result.isError).toBe(true);
191 |       expect(result.content[0].text).toContain('guideName must be a non-empty string');
192 |     });
193 |   });
194 | 
195 |   describe('error handling', () => {
196 |     beforeEach(async () => {
197 |       await initializeHandler();
198 |     });
199 | 
200 |     it('should handle client errors gracefully', async () => {
201 |       mockClient.getBestPracticeGuide.mockRejectedValue(new Error('Guide not found'));
202 | 
203 |       const result = await handler.handle('get_best_practice_guide', { guideName: 'unknown_guide' }, Date.now());
204 |       expect(result.isError).toBe(true);
205 |       expect(result.content[0].text).toContain('Guide not found');
206 |     });
207 | 
208 |     it('should throw error for unsupported tools', async () => {
209 |       await expect(handler.handle('unsupported_tool', {}, Date.now()))
210 |         .rejects.toThrow('Unsupported tool');
211 |     });
212 |   });
213 | 
214 |   describe('timing and logging', () => {
215 |     beforeEach(async () => {
216 |       await initializeHandler();
217 |       mockClient.getAvailableGuides.mockResolvedValue([]);
218 |     });
219 | 
220 |     it('should log timing information', async () => {
221 |       const startTime = Date.now();
222 |       await handler.handle('get_available_best_practice_guides', {}, startTime);
223 | 
224 |       expect(mockLogger.timing).toHaveBeenCalledWith('get_available_best_practice_guides', startTime);
225 |     });
226 | 
227 |     it('should log execution details', async () => {
228 |       await handler.handle('get_available_best_practice_guides', {}, Date.now());
229 | 
230 |       expect(mockLogger.debug).toHaveBeenCalledWith(
231 |         'get_available_best_practice_guides completed successfully',
232 |         expect.any(Object),
233 |       );
234 |     });
235 |   });
236 | });
237 | 
```

--------------------------------------------------------------------------------
/tests/servers/sfcc-mock-server/mock-data/ocapi/site-preferences-ccv.json:
--------------------------------------------------------------------------------

```json
  1 | {
  2 |   "_v": "23.2",
  3 |   "_type": "preference_value_search_result",
  4 |   "count": 5,
  5 |   "hits": [
  6 |     {
  7 |       "_type": "preference_value",
  8 |       "attribute_definition": {
  9 |         "_type": "object_attribute_definition",
 10 |         "_resource_state": "250db50d76a5e4bf869062a6ed5aab18d77aed015a880e4f0f5b8e86cc8e071d",
 11 |         "creation_date": "2025-03-06T07:14:22.000Z",
 12 |         "description": {
 13 |           "default": "Enable or disable CCV card authorization"
 14 |         },
 15 |         "display_name": {
 16 |           "default": "CCV Cards Authorise Enabled"
 17 |         },
 18 |         "effective_id": "c_ccvCardsAuthoriseEnabled",
 19 |         "externally_defined": false,
 20 |         "externally_managed": false,
 21 |         "id": "ccvCardsAuthoriseEnabled",
 22 |         "key": false,
 23 |         "last_modified": "2025-03-06T07:14:22.000Z",
 24 |         "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/SitePreferences/attribute_definitions/ccvCardsAuthoriseEnabled",
 25 |         "localizable": false,
 26 |         "mandatory": false,
 27 |         "multi_value_type": false,
 28 |         "order_required": false,
 29 |         "queryable": false,
 30 |         "read_only": false,
 31 |         "requires_encoding": false,
 32 |         "searchable": false,
 33 |         "set_value_type": false,
 34 |         "site_specific": false,
 35 |         "system": false,
 36 |         "value_type": "boolean",
 37 |         "visible": true
 38 |       },
 39 |       "description": {
 40 |         "default": "Enable or disable CCV card authorization"
 41 |       },
 42 |       "display_name": {
 43 |         "default": "CCV Cards Authorise Enabled"
 44 |       },
 45 |       "id": "ccvCardsAuthoriseEnabled",
 46 |       "site_values": {
 47 |         "RefArch": true,
 48 |         "RefArchGlobal": true,
 49 |         "pxl_1": null,
 50 |         "pxl_2": null,
 51 |         "pxl_3": null,
 52 |         "pxl_4": null,
 53 |         "pxl_5": null,
 54 |         "pxl_6": null
 55 |       },
 56 |       "value_type": "boolean"
 57 |     },
 58 |     {
 59 |       "_type": "preference_value",
 60 |       "attribute_definition": {
 61 |         "_type": "object_attribute_definition",
 62 |         "_resource_state": "34dce900d8eb50d95d97fcc0fd74449e78720a0da5a6804e561f347d8fdb5227",
 63 |         "creation_date": "2025-03-06T07:14:22.000Z",
 64 |         "description": {
 65 |           "default": "Enable storing cards in vault for CCV"
 66 |         },
 67 |         "display_name": {
 68 |           "default": "CCV Store Cards In Vault Enabled"
 69 |         },
 70 |         "effective_id": "c_ccvStoreCardsInVaultEnabled",
 71 |         "externally_defined": false,
 72 |         "externally_managed": false,
 73 |         "id": "ccvStoreCardsInVaultEnabled",
 74 |         "key": false,
 75 |         "last_modified": "2025-03-06T07:14:22.000Z",
 76 |         "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/SitePreferences/attribute_definitions/ccvStoreCardsInVaultEnabled",
 77 |         "localizable": false,
 78 |         "mandatory": false,
 79 |         "multi_value_type": false,
 80 |         "order_required": false,
 81 |         "queryable": false,
 82 |         "read_only": false,
 83 |         "requires_encoding": false,
 84 |         "searchable": false,
 85 |         "set_value_type": false,
 86 |         "site_specific": false,
 87 |         "system": false,
 88 |         "value_type": "boolean",
 89 |         "visible": true
 90 |       },
 91 |       "description": {
 92 |         "default": "Enable storing cards in vault for CCV"
 93 |       },
 94 |       "display_name": {
 95 |         "default": "CCV Store Cards In Vault Enabled"
 96 |       },
 97 |       "id": "ccvStoreCardsInVaultEnabled",
 98 |       "site_values": {
 99 |         "RefArch": false,
100 |         "RefArchGlobal": true,
101 |         "pxl_1": null,
102 |         "pxl_2": null,
103 |         "pxl_3": null,
104 |         "pxl_4": null,
105 |         "pxl_5": null,
106 |         "pxl_6": null
107 |       },
108 |       "value_type": "boolean"
109 |     },
110 |     {
111 |       "_type": "preference_value",
112 |       "attribute_definition": {
113 |         "_type": "object_attribute_definition",
114 |         "_resource_state": "bbcd6304068fcf391c849bb37952598bebed1bd55bafbe211e2a6539b4b522a9",
115 |         "creation_date": "2025-03-06T07:14:22.000Z",
116 |         "description": {
117 |           "default": "Enable automatic refunds for CCV payments"
118 |         },
119 |         "display_name": {
120 |           "default": "CCV Auto Refund Enabled"
121 |         },
122 |         "effective_id": "c_ccvAutoRefundEnabled",
123 |         "externally_defined": false,
124 |         "externally_managed": false,
125 |         "id": "ccvAutoRefundEnabled",
126 |         "key": false,
127 |         "last_modified": "2025-03-06T07:14:22.000Z",
128 |         "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/SitePreferences/attribute_definitions/ccvAutoRefundEnabled",
129 |         "localizable": false,
130 |         "mandatory": false,
131 |         "multi_value_type": false,
132 |         "order_required": false,
133 |         "queryable": false,
134 |         "read_only": false,
135 |         "requires_encoding": false,
136 |         "searchable": false,
137 |         "set_value_type": false,
138 |         "site_specific": false,
139 |         "system": false,
140 |         "value_type": "boolean",
141 |         "visible": true
142 |       },
143 |       "description": {
144 |         "default": "Enable automatic refunds for CCV payments"
145 |       },
146 |       "display_name": {
147 |         "default": "CCV Auto Refund Enabled"
148 |       },
149 |       "id": "ccvAutoRefundEnabled",
150 |       "site_values": {
151 |         "RefArch": false,
152 |         "RefArchGlobal": false,
153 |         "pxl_1": null,
154 |         "pxl_2": null,
155 |         "pxl_3": null,
156 |         "pxl_4": null,
157 |         "pxl_5": null,
158 |         "pxl_6": null
159 |       },
160 |       "value_type": "boolean"
161 |     },
162 |     {
163 |       "_type": "preference_value",
164 |       "attribute_definition": {
165 |         "_type": "object_attribute_definition",
166 |         "_resource_state": "efb6bd1843b9484576c8ffaf9447bc7d0bdee5bb6b0e9231d54804dbff5a11fa",
167 |         "creation_date": "2025-03-06T07:14:22.000Z",
168 |         "description": {
169 |           "default": "Enable SCA ready processing for CCV"
170 |         },
171 |         "display_name": {
172 |           "default": "CCV SCA Ready Enabled"
173 |         },
174 |         "effective_id": "c_ccvScaReadyEnabled",
175 |         "externally_defined": false,
176 |         "externally_managed": false,
177 |         "id": "ccvScaReadyEnabled",
178 |         "key": false,
179 |         "last_modified": "2025-03-06T07:14:22.000Z",
180 |         "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/SitePreferences/attribute_definitions/ccvScaReadyEnabled",
181 |         "localizable": false,
182 |         "mandatory": false,
183 |         "multi_value_type": false,
184 |         "order_required": false,
185 |         "queryable": false,
186 |         "read_only": false,
187 |         "requires_encoding": false,
188 |         "searchable": false,
189 |         "set_value_type": false,
190 |         "site_specific": false,
191 |         "system": false,
192 |         "value_type": "boolean",
193 |         "visible": true
194 |       },
195 |       "description": {
196 |         "default": "Enable SCA ready processing for CCV"
197 |       },
198 |       "display_name": {
199 |         "default": "CCV SCA Ready Enabled"
200 |       },
201 |       "id": "ccvScaReadyEnabled",
202 |       "site_values": {
203 |         "RefArch": true,
204 |         "RefArchGlobal": true,
205 |         "pxl_1": null,
206 |         "pxl_2": null,
207 |         "pxl_3": null,
208 |         "pxl_4": null,
209 |         "pxl_5": null,
210 |         "pxl_6": null
211 |       },
212 |       "value_type": "boolean"
213 |     },
214 |     {
215 |       "_type": "preference_value",
216 |       "attribute_definition": {
217 |         "_type": "object_attribute_definition",
218 |         "_resource_state": "ed156d5f2de1fbbea7fa8350da7b2424feb7e54ac4e338d50fd6fec992df2c6f",
219 |         "creation_date": "2025-03-06T07:14:22.000Z",
220 |         "description": {
221 |           "default": "Configuration for CCV 3DS exemption handling"
222 |         },
223 |         "display_name": {
224 |           "default": "CCV 3DS Exemption"
225 |         },
226 |         "effective_id": "c_ccv3DSExemption",
227 |         "externally_defined": false,
228 |         "externally_managed": false,
229 |         "id": "ccv3DSExemption",
230 |         "key": false,
231 |         "last_modified": "2025-03-06T07:14:22.000Z",
232 |         "link": "https://localhost:3000/s/-/dw/data/v23_2/system_object_definitions/SitePreferences/attribute_definitions/ccv3DSExemption",
233 |         "localizable": false,
234 |         "mandatory": false,
235 |         "multi_value_type": false,
236 |         "order_required": false,
237 |         "queryable": false,
238 |         "read_only": false,
239 |         "requires_encoding": false,
240 |         "searchable": false,
241 |         "set_value_type": false,
242 |         "site_specific": false,
243 |         "system": false,
244 |         "value_type": "string",
245 |         "visible": true
246 |       },
247 |       "description": {
248 |         "default": "Configuration for CCV 3DS exemption handling"
249 |       },
250 |       "display_name": {
251 |         "default": "CCV 3DS Exemption"
252 |       },
253 |       "id": "ccv3DSExemption",
254 |       "site_values": {
255 |         "RefArch": "none",
256 |         "RefArchGlobal": "low_value",
257 |         "pxl_1": null,
258 |         "pxl_2": null,
259 |         "pxl_3": null,
260 |         "pxl_4": null,
261 |         "pxl_5": null,
262 |         "pxl_6": null
263 |       },
264 |       "value_type": "string"
265 |     }
266 |   ],
267 |   "query": {
268 |     "match_all_query": {
269 |       "_type": "match_all_query"
270 |     }
271 |   },
272 |   "select": "(**)",
273 |   "start": 0,
274 |   "total": 5
275 | }
276 | 
```

--------------------------------------------------------------------------------
/tests/base-http-client.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Tests for BaseHttpClient
  3 |  * Tests the foundation HTTP client functionality
  4 |  */
  5 | 
  6 | import { BaseHttpClient } from '../src/clients/base/http-client.js';
  7 | import { Logger } from '../src/utils/logger.js';
  8 | 
  9 | // Mock fetch globally
 10 | global.fetch = jest.fn();
 11 | 
 12 | // Mock Logger
 13 | jest.mock('../src/utils/logger.js', () => ({
 14 |   Logger: {
 15 |     initialize: jest.fn(),
 16 |     getInstance: jest.fn(() => ({
 17 |       methodEntry: jest.fn(),
 18 |       methodExit: jest.fn(),
 19 |       debug: jest.fn(),
 20 |       warn: jest.fn(),
 21 |       error: jest.fn(),
 22 |       timing: jest.fn(),
 23 |       log: jest.fn(),
 24 |       info: jest.fn(),
 25 |     })),
 26 |     getChildLogger: jest.fn(() => ({
 27 |       methodEntry: jest.fn(),
 28 |       methodExit: jest.fn(),
 29 |       debug: jest.fn(),
 30 |       warn: jest.fn(),
 31 |       error: jest.fn(),
 32 |       timing: jest.fn(),
 33 |       log: jest.fn(),
 34 |       info: jest.fn(),
 35 |     })),
 36 |   },
 37 | }));
 38 | 
 39 | // Concrete implementation for testing abstract class
 40 | class TestHttpClient extends BaseHttpClient {
 41 |   private authHeaders: Record<string, string> = {};
 42 |   private shouldFailAuth = false;
 43 | 
 44 |   constructor(baseUrl: string = 'https://test-api.example.com') {
 45 |     super(baseUrl, 'TestHttpClient');
 46 |   }
 47 | 
 48 |   // Implementation of abstract method
 49 |   protected async getAuthHeaders(): Promise<Record<string, string>> {
 50 |     if (this.shouldFailAuth) {
 51 |       throw new Error('Auth failed');
 52 |     }
 53 |     return this.authHeaders;
 54 |   }
 55 | 
 56 |   // Test helpers
 57 |   setAuthHeaders(headers: Record<string, string>) {
 58 |     this.authHeaders = headers;
 59 |   }
 60 | 
 61 |   setAuthFailure(shouldFail: boolean) {
 62 |     this.shouldFailAuth = shouldFail;
 63 |   }
 64 | 
 65 |   // Expose protected methods for testing
 66 |   public async testMakeRequest<T>(endpoint: string, options?: any): Promise<T> {
 67 |     return this.makeRequest<T>(endpoint, options);
 68 |   }
 69 | 
 70 |   public async testGet<T>(endpoint: string): Promise<T> {
 71 |     return this.get<T>(endpoint);
 72 |   }
 73 | 
 74 |   public async testPost<T>(endpoint: string, data?: any): Promise<T> {
 75 |     return this.post<T>(endpoint, data);
 76 |   }
 77 | 
 78 |   public async testPut<T>(endpoint: string, data?: any): Promise<T> {
 79 |     return this.put<T>(endpoint, data);
 80 |   }
 81 | 
 82 |   public async testPatch<T>(endpoint: string, data?: any): Promise<T> {
 83 |     return this.patch<T>(endpoint, data);
 84 |   }
 85 | 
 86 |   public async testDelete<T>(endpoint: string): Promise<T> {
 87 |     return this.delete<T>(endpoint);
 88 |   }
 89 | }
 90 | 
 91 | describe('BaseHttpClient', () => {
 92 |   let client: TestHttpClient;
 93 |   let mockFetch: jest.MockedFunction<typeof fetch>;
 94 | 
 95 |   beforeEach(() => {
 96 |     jest.clearAllMocks();
 97 |     mockFetch = fetch as jest.MockedFunction<typeof fetch>;
 98 |     client = new TestHttpClient();
 99 |   });
100 | 
101 |   describe('constructor', () => {
102 |     it('should initialize with base URL and logger context', () => {
103 |       const customClient = new TestHttpClient('https://custom.api.com');
104 |       expect(customClient).toBeInstanceOf(BaseHttpClient);
105 |       expect(Logger.getChildLogger).toHaveBeenCalledWith('TestHttpClient');
106 |     });
107 |   });
108 | 
109 |   describe('makeRequest', () => {
110 |     it('should make successful GET request with auth headers', async () => {
111 |       const mockResponse = { data: 'test-data' };
112 |       client.setAuthHeaders({ 'Authorization': 'Bearer token123' });
113 | 
114 |       mockFetch.mockResolvedValue({
115 |         ok: true,
116 |         json: async () => mockResponse,
117 |       } as Response);
118 | 
119 |       const result = await client.testMakeRequest('/test-endpoint');
120 | 
121 |       expect(mockFetch).toHaveBeenCalledWith(
122 |         'https://test-api.example.com/test-endpoint',
123 |         {
124 |           headers: {
125 |             'Content-Type': 'application/json',
126 |             'Authorization': 'Bearer token123',
127 |           },
128 |         },
129 |       );
130 |       expect(result).toEqual(mockResponse);
131 |     });
132 | 
133 |     it('should handle 401 errors with retry logic', async () => {
134 |       const mockResponse = { data: 'success-after-retry' };
135 |       client.setAuthHeaders({ 'Authorization': 'Bearer old-token' });
136 | 
137 |       // First call returns 401
138 |       mockFetch
139 |         .mockResolvedValueOnce({
140 |           ok: false,
141 |           status: 401,
142 |         } as Response)
143 |         .mockResolvedValueOnce({
144 |           ok: true,
145 |           json: async () => mockResponse,
146 |         } as Response);
147 | 
148 |       const result = await client.testMakeRequest('/test-endpoint');
149 | 
150 |       expect(mockFetch).toHaveBeenCalledTimes(2);
151 |       expect(result).toEqual(mockResponse);
152 |     });
153 | 
154 |     it('should throw error for non-401 HTTP errors', async () => {
155 |       mockFetch.mockResolvedValue({
156 |         ok: false,
157 |         status: 500,
158 |         statusText: 'Internal Server Error',
159 |         text: async () => 'Server error details',
160 |       } as Response);
161 | 
162 |       await expect(client.testMakeRequest('/test-endpoint')).rejects.toThrow(
163 |         'Request failed: 500 Internal Server Error - Server error details',
164 |       );
165 |     });
166 | 
167 |     it('should handle network errors', async () => {
168 |       mockFetch.mockRejectedValue(new Error('Network error'));
169 | 
170 |       await expect(client.testMakeRequest('/test-endpoint')).rejects.toThrow(
171 |         'Network error',
172 |       );
173 |     });
174 | 
175 |     it('should handle auth header failures', async () => {
176 |       client.setAuthFailure(true);
177 | 
178 |       await expect(client.testMakeRequest('/test-endpoint')).rejects.toThrow(
179 |         'Auth failed',
180 |       );
181 |     });
182 | 
183 |     it('should merge custom headers with auth headers', async () => {
184 |       const mockResponse = { data: 'test' };
185 |       client.setAuthHeaders({ 'Authorization': 'Bearer token' });
186 | 
187 |       mockFetch.mockResolvedValue({
188 |         ok: true,
189 |         json: async () => mockResponse,
190 |       } as Response);
191 | 
192 |       await client.testMakeRequest('/test', {
193 |         headers: { 'Custom-Header': 'custom-value' },
194 |       });
195 | 
196 |       expect(mockFetch).toHaveBeenCalledWith(
197 |         'https://test-api.example.com/test',
198 |         {
199 |           headers: {
200 |             'Content-Type': 'application/json',
201 |             'Authorization': 'Bearer token',
202 |             'Custom-Header': 'custom-value',
203 |           },
204 |         },
205 |       );
206 |     });
207 |   });
208 | 
209 |   describe('HTTP method wrappers', () => {
210 |     beforeEach(() => {
211 |       client.setAuthHeaders({ 'Authorization': 'Bearer token' });
212 |       mockFetch.mockResolvedValue({
213 |         ok: true,
214 |         json: async () => ({ success: true }),
215 |       } as Response);
216 |     });
217 | 
218 |     it('should make GET request', async () => {
219 |       await client.testGet('/test');
220 | 
221 |       expect(mockFetch).toHaveBeenCalledWith(
222 |         'https://test-api.example.com/test',
223 |         expect.objectContaining({ method: 'GET' }), // GET method is explicitly set
224 |       );
225 |     });
226 | 
227 |     it('should make POST request with data', async () => {
228 |       const postData = { name: 'test' };
229 |       await client.testPost('/test', postData);
230 | 
231 |       expect(mockFetch).toHaveBeenCalledWith(
232 |         'https://test-api.example.com/test',
233 |         expect.objectContaining({
234 |           method: 'POST',
235 |           body: JSON.stringify(postData),
236 |         }),
237 |       );
238 |     });
239 | 
240 |     it('should make POST request without data', async () => {
241 |       await client.testPost('/test');
242 | 
243 |       expect(mockFetch).toHaveBeenCalledWith(
244 |         'https://test-api.example.com/test',
245 |         expect.objectContaining({
246 |           method: 'POST',
247 |         }),
248 |       );
249 |       expect(mockFetch.mock.calls[0][1]).not.toHaveProperty('body');
250 |     });
251 | 
252 |     it('should make PUT request with data', async () => {
253 |       const putData = { id: 1, name: 'updated' };
254 |       await client.testPut('/test/1', putData);
255 | 
256 |       expect(mockFetch).toHaveBeenCalledWith(
257 |         'https://test-api.example.com/test/1',
258 |         expect.objectContaining({
259 |           method: 'PUT',
260 |           body: JSON.stringify(putData),
261 |         }),
262 |       );
263 |     });
264 | 
265 |     it('should make PATCH request with data', async () => {
266 |       const patchData = { name: 'patched' };
267 |       await client.testPatch('/test/1', patchData);
268 | 
269 |       expect(mockFetch).toHaveBeenCalledWith(
270 |         'https://test-api.example.com/test/1',
271 |         expect.objectContaining({
272 |           method: 'PATCH',
273 |           body: JSON.stringify(patchData),
274 |         }),
275 |       );
276 |     });
277 | 
278 |     it('should make DELETE request', async () => {
279 |       await client.testDelete('/test/1');
280 | 
281 |       expect(mockFetch).toHaveBeenCalledWith(
282 |         'https://test-api.example.com/test/1',
283 |         expect.objectContaining({
284 |           method: 'DELETE',
285 |         }),
286 |       );
287 |     });
288 |   });
289 | 
290 |   describe('error handling during retry', () => {
291 |     it('should throw error if retry also fails', async () => {
292 |       client.setAuthHeaders({ 'Authorization': 'Bearer token' });
293 | 
294 |       mockFetch
295 |         .mockResolvedValueOnce({
296 |           ok: false,
297 |           status: 401,
298 |         } as Response)
299 |         .mockResolvedValueOnce({
300 |           ok: false,
301 |           status: 500,
302 |           statusText: 'Internal Server Error',
303 |           text: async () => 'Retry failed',
304 |         } as Response);
305 | 
306 |       await expect(client.testMakeRequest('/test')).rejects.toThrow(
307 |         'Request failed after retry: 500 Internal Server Error - Retry failed',
308 |       );
309 |     });
310 |   });
311 | });
312 | 
```

--------------------------------------------------------------------------------
/docs/dw_web/PagingModel.md:
--------------------------------------------------------------------------------

```markdown
  1 | ## Package: dw.web
  2 | 
  3 | # Class PagingModel
  4 | 
  5 | ## Inheritance Hierarchy
  6 | 
  7 | - Object
  8 |   - dw.web.PagingModel
  9 | 
 10 | ## Description
 11 | 
 12 | A page model is a helper class to apply a pages to a collection of elements or an iterator of elements and supports creating URLs for continued paging through the elements. The page model is intended to be initialized with the collection or iterator, than the paging position is applyed and than the elements are extracted with getPageElements(). In case the page model is initialized with a collection the page model can be reused multiple times.
 13 | 
 14 | ## Constants
 15 | 
 16 | ### DEFAULT_PAGE_SIZE
 17 | 
 18 | **Type:** Number = 10
 19 | 
 20 | The default page size.
 21 | 
 22 | ### MAX_PAGE_SIZE
 23 | 
 24 | **Type:** Number = 2000
 25 | 
 26 | The maximum supported page size.
 27 | 
 28 | ### PAGING_SIZE_PARAMETER
 29 | 
 30 | **Type:** String = "sz"
 31 | 
 32 | The URL Parameter used for the page size.
 33 | 
 34 | ### PAGING_START_PARAMETER
 35 | 
 36 | **Type:** String = "start"
 37 | 
 38 | The URL parameter used for the start position.
 39 | 
 40 | ## Properties
 41 | 
 42 | ### count
 43 | 
 44 | **Type:** Number (Read Only)
 45 | 
 46 | The count of the number of items in the model.
 47 | 
 48 | ### currentPage
 49 | 
 50 | **Type:** Number (Read Only)
 51 | 
 52 | The index number of the current page. The page
 53 |  counting starts with 0. The method also works with a miss-aligned
 54 |  start. In that case the start is always treated as the start of
 55 |  a page.
 56 | 
 57 | ### empty
 58 | 
 59 | **Type:** boolean (Read Only)
 60 | 
 61 | Identifies if the model is empty.
 62 | 
 63 | ### end
 64 | 
 65 | **Type:** Number (Read Only)
 66 | 
 67 | The index of the last element on the current page.
 68 | 
 69 | ### maxPage
 70 | 
 71 | **Type:** Number (Read Only)
 72 | 
 73 | The maximum possible page number. Counting for pages starts
 74 |  with 0.  The method also works with a miss-aligned start. In that case
 75 |  the returned number might be higher than ((count-1) / pageSize).
 76 | 
 77 | ### pageCount
 78 | 
 79 | **Type:** Number (Read Only)
 80 | 
 81 | The total page count. The method also works
 82 |  with a miss-aligned start. In that case the returned number might
 83 |  be higher than (count / pageSize).
 84 | 
 85 | ### pageElements
 86 | 
 87 | **Type:** Iterator (Read Only)
 88 | 
 89 | An iterator that can be used to iterate through the elements of
 90 |  the current page.
 91 | 
 92 |  In case of a collection as the page models source, the method can be
 93 |  called multiple times. Each time a fresh iterator is returned.
 94 | 
 95 |  In case of an iterator as the page models source, the method must be
 96 |  called only once. The method will always return the same iterator,
 97 |  which means the method amy return an exhausted iterator.
 98 | 
 99 | ### pageSize
100 | 
101 | **Type:** Number
102 | 
103 | The size of the page.
104 | 
105 | ### start
106 | 
107 | **Type:** Number
108 | 
109 | The current start position from which iteration will start.
110 | 
111 | ## Constructor Summary
112 | 
113 | PagingModel(elements : Iterator, count : Number) Constructs the PagingModel using the specified iterator and count value.
114 | 
115 | PagingModel(elements : Collection) Constructs the PagingModel using the specified collection.
116 | 
117 | ## Method Summary
118 | 
119 | ### appendPageSize
120 | 
121 | **Signature:** `static appendPageSize(url : URL, pageSize : Number) : URL`
122 | 
123 | Returns an URL containing the page size parameter appended to the specified url.
124 | 
125 | ### appendPaging
126 | 
127 | **Signature:** `appendPaging(url : URL) : URL`
128 | 
129 | Returns an URL by appending the current page start position and the current page size to the URL.
130 | 
131 | ### appendPaging
132 | 
133 | **Signature:** `appendPaging(url : URL, position : Number) : URL`
134 | 
135 | Returns an URL by appending the paging parameters for a desired page start position and the current page size to the specified url.
136 | 
137 | ### getCount
138 | 
139 | **Signature:** `getCount() : Number`
140 | 
141 | Returns the count of the number of items in the model.
142 | 
143 | ### getCurrentPage
144 | 
145 | **Signature:** `getCurrentPage() : Number`
146 | 
147 | Returns the index number of the current page.
148 | 
149 | ### getEnd
150 | 
151 | **Signature:** `getEnd() : Number`
152 | 
153 | Returns the index of the last element on the current page.
154 | 
155 | ### getMaxPage
156 | 
157 | **Signature:** `getMaxPage() : Number`
158 | 
159 | Returns the maximum possible page number.
160 | 
161 | ### getPageCount
162 | 
163 | **Signature:** `getPageCount() : Number`
164 | 
165 | Returns the total page count.
166 | 
167 | ### getPageElements
168 | 
169 | **Signature:** `getPageElements() : Iterator`
170 | 
171 | Returns an iterator that can be used to iterate through the elements of the current page.
172 | 
173 | ### getPageSize
174 | 
175 | **Signature:** `getPageSize() : Number`
176 | 
177 | Returns the size of the page.
178 | 
179 | ### getStart
180 | 
181 | **Signature:** `getStart() : Number`
182 | 
183 | Returns the current start position from which iteration will start.
184 | 
185 | ### isEmpty
186 | 
187 | **Signature:** `isEmpty() : boolean`
188 | 
189 | Identifies if the model is empty.
190 | 
191 | ### setPageSize
192 | 
193 | **Signature:** `setPageSize(pageSize : Number) : void`
194 | 
195 | Sets the size of the page.
196 | 
197 | ### setStart
198 | 
199 | **Signature:** `setStart(start : Number) : void`
200 | 
201 | Sets the current start position from which iteration will start.
202 | 
203 | ## Constructor Detail
204 | 
205 | ## Method Detail
206 | 
207 | ## Method Details
208 | 
209 | ### appendPageSize
210 | 
211 | **Signature:** `static appendPageSize(url : URL, pageSize : Number) : URL`
212 | 
213 | **Description:** Returns an URL containing the page size parameter appended to the specified url. The name of the page size parameter is 'sz' (see PAGE_SIZE_PARAMETER). The start position parameter is not appended to the returned URL.
214 | 
215 | **Parameters:**
216 | 
217 | - `url`: the URL to append the page size parameter to.
218 | - `pageSize`: the page size
219 | 
220 | **Returns:**
221 | 
222 | an URL that contains the page size parameter.
223 | 
224 | ---
225 | 
226 | ### appendPaging
227 | 
228 | **Signature:** `appendPaging(url : URL) : URL`
229 | 
230 | **Description:** Returns an URL by appending the current page start position and the current page size to the URL.
231 | 
232 | **Parameters:**
233 | 
234 | - `url`: the URL to append the current paging position to.
235 | 
236 | **Returns:**
237 | 
238 | an URL containing the current paging position.
239 | 
240 | ---
241 | 
242 | ### appendPaging
243 | 
244 | **Signature:** `appendPaging(url : URL, position : Number) : URL`
245 | 
246 | **Description:** Returns an URL by appending the paging parameters for a desired page start position and the current page size to the specified url. The name of the page start position parameter is 'start' (see PAGING_START_PARAMETER) and the page size parameter is 'sz' (see PAGE_SIZE_PARAMETER).
247 | 
248 | **Parameters:**
249 | 
250 | - `url`: the URL to append the paging parameter to.
251 | - `position`: the start position.
252 | 
253 | **Returns:**
254 | 
255 | an URL that contains the paging parameters.
256 | 
257 | ---
258 | 
259 | ### getCount
260 | 
261 | **Signature:** `getCount() : Number`
262 | 
263 | **Description:** Returns the count of the number of items in the model.
264 | 
265 | **Returns:**
266 | 
267 | the count of the number of items in the model.
268 | 
269 | ---
270 | 
271 | ### getCurrentPage
272 | 
273 | **Signature:** `getCurrentPage() : Number`
274 | 
275 | **Description:** Returns the index number of the current page. The page counting starts with 0. The method also works with a miss-aligned start. In that case the start is always treated as the start of a page.
276 | 
277 | **Returns:**
278 | 
279 | the index number of the current page.
280 | 
281 | ---
282 | 
283 | ### getEnd
284 | 
285 | **Signature:** `getEnd() : Number`
286 | 
287 | **Description:** Returns the index of the last element on the current page.
288 | 
289 | **Returns:**
290 | 
291 | the index of the last element on the current page.
292 | 
293 | ---
294 | 
295 | ### getMaxPage
296 | 
297 | **Signature:** `getMaxPage() : Number`
298 | 
299 | **Description:** Returns the maximum possible page number. Counting for pages starts with 0. The method also works with a miss-aligned start. In that case the returned number might be higher than ((count-1) / pageSize).
300 | 
301 | **Returns:**
302 | 
303 | the maximum possible page number.
304 | 
305 | ---
306 | 
307 | ### getPageCount
308 | 
309 | **Signature:** `getPageCount() : Number`
310 | 
311 | **Description:** Returns the total page count. The method also works with a miss-aligned start. In that case the returned number might be higher than (count / pageSize).
312 | 
313 | **Returns:**
314 | 
315 | the total page count.
316 | 
317 | ---
318 | 
319 | ### getPageElements
320 | 
321 | **Signature:** `getPageElements() : Iterator`
322 | 
323 | **Description:** Returns an iterator that can be used to iterate through the elements of the current page. In case of a collection as the page models source, the method can be called multiple times. Each time a fresh iterator is returned. In case of an iterator as the page models source, the method must be called only once. The method will always return the same iterator, which means the method amy return an exhausted iterator.
324 | 
325 | **Returns:**
326 | 
327 | an iterator that you use to iterate through the elements of the current page.
328 | 
329 | ---
330 | 
331 | ### getPageSize
332 | 
333 | **Signature:** `getPageSize() : Number`
334 | 
335 | **Description:** Returns the size of the page.
336 | 
337 | **Returns:**
338 | 
339 | the size of the page.
340 | 
341 | ---
342 | 
343 | ### getStart
344 | 
345 | **Signature:** `getStart() : Number`
346 | 
347 | **Description:** Returns the current start position from which iteration will start.
348 | 
349 | **Returns:**
350 | 
351 | the current start position from which iteration will start.
352 | 
353 | ---
354 | 
355 | ### isEmpty
356 | 
357 | **Signature:** `isEmpty() : boolean`
358 | 
359 | **Description:** Identifies if the model is empty.
360 | 
361 | **Returns:**
362 | 
363 | true if the model is empty, false otherwise.
364 | 
365 | ---
366 | 
367 | ### setPageSize
368 | 
369 | **Signature:** `setPageSize(pageSize : Number) : void`
370 | 
371 | **Description:** Sets the size of the page. The page size must be greater or equal to 1.
372 | 
373 | **Parameters:**
374 | 
375 | - `pageSize`: the size of the page.
376 | 
377 | ---
378 | 
379 | ### setStart
380 | 
381 | **Signature:** `setStart(start : Number) : void`
382 | 
383 | **Description:** Sets the current start position from which iteration will start.
384 | 
385 | **Parameters:**
386 | 
387 | - `start`: the current start position from which iteration will start.
388 | 
389 | ---
```

--------------------------------------------------------------------------------
/tests/mcp/yaml/get-hook-reference.docs-only.test.mcp.yml:
--------------------------------------------------------------------------------

```yaml
  1 | description: "get_hook_reference docs-only tests"
  2 | 
  3 | # Chosen as next untested docs-only tool (no existing YAML test file). Covers success (ocapi_hooks, scapi_hooks), empty result (invalid guideName), structure validation, field extraction, and performance.
  4 | 
  5 | tests:
  6 |   - it: "should list tools include get_hook_reference"
  7 |     request:
  8 |       jsonrpc: "2.0"
  9 |       id: "list-hooks-tool"
 10 |       method: "tools/list"
 11 |       params: {}
 12 |     expect:
 13 |       response:
 14 |         jsonrpc: "2.0"
 15 |         id: "list-hooks-tool"
 16 |         result:
 17 |           tools: "match:type:array"
 18 |           match:extractField: "tools.*.name"
 19 |           value: "match:arrayContains:get_hook_reference"
 20 |       stderr: "toBeEmpty"
 21 | 
 22 |   - it: "should retrieve OCAPI hook reference with categories"
 23 |     request:
 24 |       jsonrpc: "2.0"
 25 |       id: "ocapi-hooks-1"
 26 |       method: "tools/call"
 27 |       params:
 28 |         name: "get_hook_reference"
 29 |         arguments:
 30 |           guideName: "ocapi_hooks"
 31 |     expect:
 32 |       response:
 33 |         jsonrpc: "2.0"
 34 |         id: "ocapi-hooks-1"
 35 |         result:
 36 |           content:
 37 |             match:arrayElements:
 38 |               match:partial:
 39 |                 type: "text"
 40 |                 text: "match:contains:Shop API Hooks"
 41 |           isError: false
 42 |       performance:
 43 |         maxResponseTime: "1200ms"
 44 |       stderr: "toBeEmpty"
 45 | 
 46 |   - it: "should retrieve SCAPI hook reference including signatures"
 47 |     request:
 48 |       jsonrpc: "2.0"
 49 |       id: "scapi-hooks-1"
 50 |       method: "tools/call"
 51 |       params:
 52 |         name: "get_hook_reference"
 53 |         arguments:
 54 |           guideName: "scapi_hooks"
 55 |     expect:
 56 |       response:
 57 |         jsonrpc: "2.0"
 58 |         id: "scapi-hooks-1"
 59 |         result:
 60 |           content:
 61 |             match:arrayElements:
 62 |               match:partial:
 63 |                 type: "text"
 64 |                 text: "match:contains:Shopper Baskets API Hooks"
 65 |           isError: false
 66 |       performance:
 67 |         maxResponseTime: "1200ms"
 68 |       stderr: "toBeEmpty"
 69 | 
 70 |   - it: "should return empty array content for invalid guideName without error"
 71 |     request:
 72 |       jsonrpc: "2.0"
 73 |       id: "invalid-hooks-1"
 74 |       method: "tools/call"
 75 |       params:
 76 |         name: "get_hook_reference"
 77 |         arguments:
 78 |           guideName: "invalid_hooks"
 79 |     expect:
 80 |       response:
 81 |         jsonrpc: "2.0"
 82 |         id: "invalid-hooks-1"
 83 |         result:
 84 |           content:
 85 |             - type: "text"
 86 |               text: "match:regex:^\\[\\s*\\]$"
 87 |           isError: false
 88 |       performance:
 89 |         maxResponseTime: "800ms"
 90 |       stderr: "toBeEmpty"
 91 | 
 92 |   - it: "should contain at least one hook endpoint pattern in OCAPI response"
 93 |     request:
 94 |       jsonrpc: "2.0"
 95 |       id: "ocapi-hooks-endpoint-pattern"
 96 |       method: "tools/call"
 97 |       params:
 98 |         name: "get_hook_reference"
 99 |         arguments:
100 |           guideName: "ocapi_hooks"
101 |     expect:
102 |       response:
103 |         jsonrpc: "2.0"
104 |         id: "ocapi-hooks-endpoint-pattern"
105 |         result:
106 |           content:
107 |             match:arrayElements:
108 |               match:partial:
109 |                 text: "match:regex:[\\s\\S]*GET /products/\\{id\\}[\\s\\S]*"
110 |           isError: false
111 |       performance:
112 |         maxResponseTime: "1200ms"
113 |       stderr: "toBeEmpty"
114 | 
115 |   - it: "should contain at least one SCAPI signature pattern in response"
116 |     request:
117 |       jsonrpc: "2.0"
118 |       id: "scapi-hooks-signature-pattern"
119 |       method: "tools/call"
120 |       params:
121 |         name: "get_hook_reference"
122 |         arguments:
123 |           guideName: "scapi_hooks"
124 |     expect:
125 |       response:
126 |         jsonrpc: "2.0"
127 |         id: "scapi-hooks-signature-pattern"
128 |         result:
129 |           content:
130 |             match:arrayElements:
131 |               match:partial:
132 |                 text: "match:contains:modifyPOSTResponse(basket : dw.order.Basket"
133 |           isError: false
134 |       performance:
135 |         maxResponseTime: "1200ms"
136 |       stderr: "toBeEmpty"
137 | 
138 |   - it: "should reject missing guideName with error flag and message"
139 |     request:
140 |       jsonrpc: "2.0"
141 |       id: "missing-param-1"
142 |       method: "tools/call"
143 |       params:
144 |         name: "get_hook_reference"
145 |         arguments: {}
146 |     expect:
147 |       response:
148 |         jsonrpc: "2.0"
149 |         id: "missing-param-1"
150 |         result:
151 |           content:
152 |             match:arrayElements:
153 |               match:partial:
154 |                 type: "text"
155 |                 text: "match:contains:guideName must be a non-empty string"
156 |           isError: true
157 |       performance:
158 |         maxResponseTime: "600ms"
159 |       stderr: "toBeEmpty"
160 | 
161 |   - it: "should reject empty guideName with same validation error"
162 |     request:
163 |       jsonrpc: "2.0"
164 |       id: "empty-param-1"
165 |       method: "tools/call"
166 |       params:
167 |         name: "get_hook_reference"
168 |         arguments:
169 |           guideName: ""
170 |     expect:
171 |       response:
172 |         jsonrpc: "2.0"
173 |         id: "empty-param-1"
174 |         result:
175 |           content:
176 |             match:arrayElements:
177 |               match:partial:
178 |                 type: "text"
179 |                 text: "match:contains:guideName must be a non-empty string"
180 |           isError: true
181 |       performance:
182 |         maxResponseTime: "600ms"
183 |       stderr: "toBeEmpty"
184 | 
185 |   - it: "OCAPI response should include multiple categories"
186 |     request:
187 |       jsonrpc: "2.0"
188 |       id: "ocapi-categories-1"
189 |       method: "tools/call"
190 |       params:
191 |         name: "get_hook_reference"
192 |         arguments:
193 |           guideName: "ocapi_hooks"
194 |     expect:
195 |       response:
196 |         jsonrpc: "2.0"
197 |         id: "ocapi-categories-1"
198 |         result:
199 |           content:
200 |             match:arrayElements:
201 |               match:partial:
202 |                 text: "match:contains:Data API Hooks"
203 |           isError: false
204 |       performance:
205 |         maxResponseTime: "1200ms"
206 |       stderr: "toBeEmpty"
207 | 
208 |   - it: "SCAPI response should include multiple categories"
209 |     request:
210 |       jsonrpc: "2.0"
211 |       id: "scapi-categories-1"
212 |       method: "tools/call"
213 |       params:
214 |         name: "get_hook_reference"
215 |         arguments:
216 |           guideName: "scapi_hooks"
217 |     expect:
218 |       response:
219 |         jsonrpc: "2.0"
220 |         id: "scapi-categories-1"
221 |         result:
222 |           content:
223 |             match:arrayElements:
224 |               match:partial:
225 |                 text: "match:contains:Shopper Orders API Hooks"
226 |           isError: false
227 |       performance:
228 |         maxResponseTime: "1200ms"
229 |       stderr: "toBeEmpty"
230 | 
231 |   - it: "SCAPI response should contain multiple distinct hook signatures"
232 |     request:
233 |       jsonrpc: "2.0"
234 |       id: "scapi-signatures-1"
235 |       method: "tools/call"
236 |       params:
237 |         name: "get_hook_reference"
238 |         arguments:
239 |           guideName: "scapi_hooks"
240 |     expect:
241 |       response:
242 |         jsonrpc: "2.0"
243 |         id: "scapi-signatures-1"
244 |         result:
245 |           content:
246 |             match:arrayElements:
247 |               match:partial:
248 |                 text: "match:regex:[\\s\\S]*beforePOST\\(basket : dw.order.Basket[\\s\\S]*afterPOST\\(basket : dw.order.Basket[\\s\\S]*"
249 |           isError: false
250 |       performance:
251 |         maxResponseTime: "1500ms"
252 |       stderr: "toBeEmpty"
253 | 
254 |   - it: "OCAPI response should have multiple hookPoints for basket POST"
255 |     request:
256 |       jsonrpc: "2.0"
257 |       id: "ocapi-multiple-hookpoints-1"
258 |       method: "tools/call"
259 |       params:
260 |         name: "get_hook_reference"
261 |         arguments:
262 |           guideName: "ocapi_hooks"
263 |     expect:
264 |       response:
265 |         jsonrpc: "2.0"
266 |         id: "ocapi-multiple-hookpoints-1"
267 |         result:
268 |           content:
269 |             match:arrayElements:
270 |               match:partial:
271 |                 text: "match:regex:[\\s\\S]*POST /baskets[\\s\\S]*modifyPOSTResponse[\\s\\S]*validateBasket[\\s\\S]*"
272 |           isError: false
273 |       performance:
274 |         maxResponseTime: "1500ms"
275 |       stderr: "toBeEmpty"
276 | 
277 |   - it: "SCAPI response should NOT contain unrelated error text"
278 |     request:
279 |       jsonrpc: "2.0"
280 |       id: "scapi-negative-1"
281 |       method: "tools/call"
282 |       params:
283 |         name: "get_hook_reference"
284 |         arguments:
285 |           guideName: "scapi_hooks"
286 |     expect:
287 |       response:
288 |         jsonrpc: "2.0"
289 |         id: "scapi-negative-1"
290 |         result:
291 |           content:
292 |             match:arrayElements:
293 |               match:partial:
294 |                 text: "match:not:contains:guideName must be a non-empty string"
295 |           isError: false
296 |       performance:
297 |         maxResponseTime: "1200ms"
298 |       stderr: "toBeEmpty"
299 | 
300 |   - it: "SCAPI content payload should be reasonably large (length > 500 chars)"
301 |     request:
302 |       jsonrpc: "2.0"
303 |       id: "scapi-size-1"
304 |       method: "tools/call"
305 |       params:
306 |         name: "get_hook_reference"
307 |         arguments:
308 |           guideName: "scapi_hooks"
309 |     expect:
310 |       response:
311 |         jsonrpc: "2.0"
312 |         id: "scapi-size-1"
313 |         result:
314 |           content:
315 |             match:arrayElements:
316 |               match:partial:
317 |                 text: "match:regex:^[\\s\\S]{500,}$"
318 |           isError: false
319 |       performance:
320 |         maxResponseTime: "1500ms"
321 |       stderr: "toBeEmpty"
322 | 
```

--------------------------------------------------------------------------------
/tests/mcp/yaml/get-hook-reference.full-mode.test.mcp.yml:
--------------------------------------------------------------------------------

```yaml
  1 | description: "get_hook_reference full-mode tests"
  2 | 
  3 | # Chosen as next untested full-mode tool (no existing YAML test file). Covers success (ocapi_hooks, scapi_hooks), empty result (invalid guideName), structure validation, field extraction, and performance.
  4 | 
  5 | tests:
  6 |   - it: "should list tools include get_hook_reference"
  7 |     request:
  8 |       jsonrpc: "2.0"
  9 |       id: "list-hooks-tool"
 10 |       method: "tools/list"
 11 |       params: {}
 12 |     expect:
 13 |       response:
 14 |         jsonrpc: "2.0"
 15 |         id: "list-hooks-tool"
 16 |         result:
 17 |           tools: "match:type:array"
 18 |           match:extractField: "tools.*.name"
 19 |           value: "match:arrayContains:get_hook_reference"
 20 |       stderr: "toBeEmpty"
 21 | 
 22 |   - it: "should retrieve OCAPI hook reference with categories"
 23 |     request:
 24 |       jsonrpc: "2.0"
 25 |       id: "ocapi-hooks-1"
 26 |       method: "tools/call"
 27 |       params:
 28 |         name: "get_hook_reference"
 29 |         arguments:
 30 |           guideName: "ocapi_hooks"
 31 |     expect:
 32 |       response:
 33 |         jsonrpc: "2.0"
 34 |         id: "ocapi-hooks-1"
 35 |         result:
 36 |           content:
 37 |             match:arrayElements:
 38 |               match:partial:
 39 |                 type: "text"
 40 |                 text: "match:contains:Shop API Hooks"
 41 |           isError: false
 42 |       performance:
 43 |         maxResponseTime: "1200ms"
 44 |       stderr: "toBeEmpty"
 45 | 
 46 |   - it: "should retrieve SCAPI hook reference including signatures"
 47 |     request:
 48 |       jsonrpc: "2.0"
 49 |       id: "scapi-hooks-1"
 50 |       method: "tools/call"
 51 |       params:
 52 |         name: "get_hook_reference"
 53 |         arguments:
 54 |           guideName: "scapi_hooks"
 55 |     expect:
 56 |       response:
 57 |         jsonrpc: "2.0"
 58 |         id: "scapi-hooks-1"
 59 |         result:
 60 |           content:
 61 |             match:arrayElements:
 62 |               match:partial:
 63 |                 type: "text"
 64 |                 text: "match:contains:Shopper Baskets API Hooks"
 65 |           isError: false
 66 |       performance:
 67 |         maxResponseTime: "1200ms"
 68 |       stderr: "toBeEmpty"
 69 | 
 70 |   - it: "should return empty array content for invalid guideName without error"
 71 |     request:
 72 |       jsonrpc: "2.0"
 73 |       id: "invalid-hooks-1"
 74 |       method: "tools/call"
 75 |       params:
 76 |         name: "get_hook_reference"
 77 |         arguments:
 78 |           guideName: "invalid_hooks"
 79 |     expect:
 80 |       response:
 81 |         jsonrpc: "2.0"
 82 |         id: "invalid-hooks-1"
 83 |         result:
 84 |           content:
 85 |             - type: "text"
 86 |               text: "match:regex:^\\[\\s*\\]$"
 87 |           isError: false
 88 |       performance:
 89 |         maxResponseTime: "800ms"
 90 |       stderr: "toBeEmpty"
 91 | 
 92 |   - it: "should contain at least one hook endpoint pattern in OCAPI response"
 93 |     request:
 94 |       jsonrpc: "2.0"
 95 |       id: "ocapi-hooks-endpoint-pattern"
 96 |       method: "tools/call"
 97 |       params:
 98 |         name: "get_hook_reference"
 99 |         arguments:
100 |           guideName: "ocapi_hooks"
101 |     expect:
102 |       response:
103 |         jsonrpc: "2.0"
104 |         id: "ocapi-hooks-endpoint-pattern"
105 |         result:
106 |           content:
107 |             match:arrayElements:
108 |               match:partial:
109 |                 text: "match:regex:[\\s\\S]*GET /products/\\{id\\}[\\s\\S]*"
110 |           isError: false
111 |       performance:
112 |         maxResponseTime: "1200ms"
113 |       stderr: "toBeEmpty"
114 | 
115 |   - it: "should contain at least one SCAPI signature pattern in response"
116 |     request:
117 |       jsonrpc: "2.0"
118 |       id: "scapi-hooks-signature-pattern"
119 |       method: "tools/call"
120 |       params:
121 |         name: "get_hook_reference"
122 |         arguments:
123 |           guideName: "scapi_hooks"
124 |     expect:
125 |       response:
126 |         jsonrpc: "2.0"
127 |         id: "scapi-hooks-signature-pattern"
128 |         result:
129 |           content:
130 |             match:arrayElements:
131 |               match:partial:
132 |                 text: "match:contains:modifyPOSTResponse(basket : dw.order.Basket"
133 |           isError: false
134 |       performance:
135 |         maxResponseTime: "1200ms"
136 |       stderr: "toBeEmpty"
137 | 
138 |   - it: "should reject missing guideName with error flag and message"
139 |     request:
140 |       jsonrpc: "2.0"
141 |       id: "missing-param-1"
142 |       method: "tools/call"
143 |       params:
144 |         name: "get_hook_reference"
145 |         arguments: {}
146 |     expect:
147 |       response:
148 |         jsonrpc: "2.0"
149 |         id: "missing-param-1"
150 |         result:
151 |           content:
152 |             match:arrayElements:
153 |               match:partial:
154 |                 type: "text"
155 |                 text: "match:contains:guideName must be a non-empty string"
156 |           isError: true
157 |       performance:
158 |         maxResponseTime: "600ms"
159 |       stderr: "toBeEmpty"
160 | 
161 |   - it: "should reject empty guideName with same validation error"
162 |     request:
163 |       jsonrpc: "2.0"
164 |       id: "empty-param-1"
165 |       method: "tools/call"
166 |       params:
167 |         name: "get_hook_reference"
168 |         arguments:
169 |           guideName: ""
170 |     expect:
171 |       response:
172 |         jsonrpc: "2.0"
173 |         id: "empty-param-1"
174 |         result:
175 |           content:
176 |             match:arrayElements:
177 |               match:partial:
178 |                 type: "text"
179 |                 text: "match:contains:guideName must be a non-empty string"
180 |           isError: true
181 |       performance:
182 |         maxResponseTime: "600ms"
183 |       stderr: "toBeEmpty"
184 | 
185 |   - it: "OCAPI response should include multiple categories"
186 |     request:
187 |       jsonrpc: "2.0"
188 |       id: "ocapi-categories-1"
189 |       method: "tools/call"
190 |       params:
191 |         name: "get_hook_reference"
192 |         arguments:
193 |           guideName: "ocapi_hooks"
194 |     expect:
195 |       response:
196 |         jsonrpc: "2.0"
197 |         id: "ocapi-categories-1"
198 |         result:
199 |           content:
200 |             match:arrayElements:
201 |               match:partial:
202 |                 text: "match:contains:Data API Hooks"
203 |           isError: false
204 |       performance:
205 |         maxResponseTime: "1200ms"
206 |       stderr: "toBeEmpty"
207 | 
208 |   - it: "SCAPI response should include multiple categories"
209 |     request:
210 |       jsonrpc: "2.0"
211 |       id: "scapi-categories-1"
212 |       method: "tools/call"
213 |       params:
214 |         name: "get_hook_reference"
215 |         arguments:
216 |           guideName: "scapi_hooks"
217 |     expect:
218 |       response:
219 |         jsonrpc: "2.0"
220 |         id: "scapi-categories-1"
221 |         result:
222 |           content:
223 |             match:arrayElements:
224 |               match:partial:
225 |                 text: "match:contains:Shopper Orders API Hooks"
226 |           isError: false
227 |       performance:
228 |         maxResponseTime: "1200ms"
229 |       stderr: "toBeEmpty"
230 | 
231 |   - it: "SCAPI response should contain multiple distinct hook signatures"
232 |     request:
233 |       jsonrpc: "2.0"
234 |       id: "scapi-signatures-1"
235 |       method: "tools/call"
236 |       params:
237 |         name: "get_hook_reference"
238 |         arguments:
239 |           guideName: "scapi_hooks"
240 |     expect:
241 |       response:
242 |         jsonrpc: "2.0"
243 |         id: "scapi-signatures-1"
244 |         result:
245 |           content:
246 |             match:arrayElements:
247 |               match:partial:
248 |                 text: "match:regex:[\\s\\S]*beforePOST\\(basket : dw.order.Basket[\\s\\S]*afterPOST\\(basket : dw.order.Basket[\\s\\S]*"
249 |           isError: false
250 |       performance:
251 |         maxResponseTime: "1500ms"
252 |       stderr: "toBeEmpty"
253 | 
254 |   - it: "OCAPI response should have multiple hookPoints for basket POST"
255 |     request:
256 |       jsonrpc: "2.0"
257 |       id: "ocapi-multiple-hookpoints-1"
258 |       method: "tools/call"
259 |       params:
260 |         name: "get_hook_reference"
261 |         arguments:
262 |           guideName: "ocapi_hooks"
263 |     expect:
264 |       response:
265 |         jsonrpc: "2.0"
266 |         id: "ocapi-multiple-hookpoints-1"
267 |         result:
268 |           content:
269 |             match:arrayElements:
270 |               match:partial:
271 |                 text: "match:regex:[\\s\\S]*POST /baskets[\\s\\S]*modifyPOSTResponse[\\s\\S]*validateBasket[\\s\\S]*"
272 |           isError: false
273 |       performance:
274 |         maxResponseTime: "1500ms"
275 |       stderr: "toBeEmpty"
276 | 
277 |   - it: "SCAPI response should NOT contain unrelated error text"
278 |     request:
279 |       jsonrpc: "2.0"
280 |       id: "scapi-negative-1"
281 |       method: "tools/call"
282 |       params:
283 |         name: "get_hook_reference"
284 |         arguments:
285 |           guideName: "scapi_hooks"
286 |     expect:
287 |       response:
288 |         jsonrpc: "2.0"
289 |         id: "scapi-negative-1"
290 |         result:
291 |           content:
292 |             match:arrayElements:
293 |               match:partial:
294 |                 text: "match:not:contains:guideName must be a non-empty string"
295 |           isError: false
296 |       performance:
297 |         maxResponseTime: "1200ms"
298 |       stderr: "toBeEmpty"
299 | 
300 |   - it: "SCAPI content payload should be reasonably large (length > 500 chars)"
301 |     request:
302 |       jsonrpc: "2.0"
303 |       id: "scapi-size-1"
304 |       method: "tools/call"
305 |       params:
306 |         name: "get_hook_reference"
307 |         arguments:
308 |           guideName: "scapi_hooks"
309 |     expect:
310 |       response:
311 |         jsonrpc: "2.0"
312 |         id: "scapi-size-1"
313 |         result:
314 |           content:
315 |             match:arrayElements:
316 |               match:partial:
317 |                 text: "match:regex:^[\\s\\S]{500,}$"
318 |           isError: false
319 |       performance:
320 |         maxResponseTime: "1500ms"
321 |       stderr: "toBeEmpty"
322 | 
```

--------------------------------------------------------------------------------
/docs/TopLevel/DataView.md:
--------------------------------------------------------------------------------

```markdown
  1 | ## Package: TopLevel
  2 | 
  3 | # Class DataView
  4 | 
  5 | ## Inheritance Hierarchy
  6 | 
  7 | - Object
  8 |   - DataView
  9 | 
 10 | ## Description
 11 | 
 12 | The DataView provides low level access to ArrayBuffer.
 13 | 
 14 | ## Properties
 15 | 
 16 | ### buffer
 17 | 
 18 | **Type:** ArrayBuffer
 19 | 
 20 | The array buffer referenced by this view.
 21 | 
 22 | ### byteLength
 23 | 
 24 | **Type:** Number
 25 | 
 26 | The number of bytes in the array buffer used by this view.
 27 | 
 28 | ### byteOffset
 29 | 
 30 | **Type:** Number
 31 | 
 32 | The start offset for this view within the array buffer.
 33 | 
 34 | ## Constructor Summary
 35 | 
 36 | DataView(buffer : ArrayBuffer, byteOffset : Number, byteLength : Number) Creates a data view on the given ArrayBuffer.
 37 | 
 38 | ## Method Summary
 39 | 
 40 | ### getFloat32
 41 | 
 42 | **Signature:** `getFloat32(byteOffset : Number, littleEndian : boolean) : Number`
 43 | 
 44 | Returns the 32-bit floating point number at the given offset.
 45 | 
 46 | ### getFloat64
 47 | 
 48 | **Signature:** `getFloat64(byteOffset : Number, littleEndian : boolean) : Number`
 49 | 
 50 | Returns the 64-bit floating point number at the given offset.
 51 | 
 52 | ### getInt16
 53 | 
 54 | **Signature:** `getInt16(byteOffset : Number, littleEndian : boolean) : Number`
 55 | 
 56 | Returns the 16-bit signed integer number at the given offset.
 57 | 
 58 | ### getInt32
 59 | 
 60 | **Signature:** `getInt32(byteOffset : Number, littleEndian : boolean) : Number`
 61 | 
 62 | Returns the 32-bit signed integer number at the given offset.
 63 | 
 64 | ### getInt8
 65 | 
 66 | **Signature:** `getInt8(byteOffset : Number) : Number`
 67 | 
 68 | Returns the 8-bit signed integer number at the given offset.
 69 | 
 70 | ### getUint16
 71 | 
 72 | **Signature:** `getUint16(byteOffset : Number, littleEndian : boolean) : Number`
 73 | 
 74 | Returns the 16-bit unsigned integer number at the given offset.
 75 | 
 76 | ### getUint32
 77 | 
 78 | **Signature:** `getUint32(byteOffset : Number, littleEndian : boolean) : Number`
 79 | 
 80 | Returns the 32-bit unsigned integer number at the given offset.
 81 | 
 82 | ### getUint8
 83 | 
 84 | **Signature:** `getUint8(byteOffset : Number) : Number`
 85 | 
 86 | Returns the 8-bit unsigned integer number at the given offset.
 87 | 
 88 | ### setFloat32
 89 | 
 90 | **Signature:** `setFloat32(byteOffset : Number, value : Number, littleEndian : boolean) : void`
 91 | 
 92 | Writes a 32-bit floating point number into the byte array at the given offset.
 93 | 
 94 | ### setFloat64
 95 | 
 96 | **Signature:** `setFloat64(byteOffset : Number, value : Number, littleEndian : boolean) : void`
 97 | 
 98 | Writes a 64-bit floating point number into the byte array at the given offset.
 99 | 
100 | ### setInt16
101 | 
102 | **Signature:** `setInt16(byteOffset : Number, value : Number, littleEndian : boolean) : void`
103 | 
104 | Writes a 16-bit signed integer number into the byte array at the given offset.
105 | 
106 | ### setInt32
107 | 
108 | **Signature:** `setInt32(byteOffset : Number, value : Number, littleEndian : boolean) : void`
109 | 
110 | Writes a 32-bit signed integer number into the byte array at the given offset.
111 | 
112 | ### setInt8
113 | 
114 | **Signature:** `setInt8(byteOffset : Number, value : Number) : void`
115 | 
116 | Writes an 8-bit signed integer number into the byte array at the given offset.
117 | 
118 | ### setUint16
119 | 
120 | **Signature:** `setUint16(byteOffset : Number, value : Number, littleEndian : boolean) : void`
121 | 
122 | Writes a 16-bit unsigned integer number into the byte array at the given offset.
123 | 
124 | ### setUint32
125 | 
126 | **Signature:** `setUint32(byteOffset : Number, value : Number, littleEndian : boolean) : void`
127 | 
128 | Writes a 32-bit unsigned integer number into the byte array at the given offset.
129 | 
130 | ### setUint8
131 | 
132 | **Signature:** `setUint8(byteOffset : Number, value : Number) : void`
133 | 
134 | Writes an 8-bit unsigned integer number into the byte array at the given offset.
135 | 
136 | ## Constructor Detail
137 | 
138 | ## Method Detail
139 | 
140 | ## Method Details
141 | 
142 | ### getFloat32
143 | 
144 | **Signature:** `getFloat32(byteOffset : Number, littleEndian : boolean) : Number`
145 | 
146 | **Description:** Returns the 32-bit floating point number at the given offset.
147 | 
148 | **Parameters:**
149 | 
150 | - `byteOffset`: The offset within the view.
151 | - `littleEndian`: Optional. Default is false. Use true if the number is stored in little-endian format.
152 | 
153 | ---
154 | 
155 | ### getFloat64
156 | 
157 | **Signature:** `getFloat64(byteOffset : Number, littleEndian : boolean) : Number`
158 | 
159 | **Description:** Returns the 64-bit floating point number at the given offset.
160 | 
161 | **Parameters:**
162 | 
163 | - `byteOffset`: The offset within the view.
164 | - `littleEndian`: Optional. Default is false. Use true if the number is stored in little-endian format.
165 | 
166 | ---
167 | 
168 | ### getInt16
169 | 
170 | **Signature:** `getInt16(byteOffset : Number, littleEndian : boolean) : Number`
171 | 
172 | **Description:** Returns the 16-bit signed integer number at the given offset.
173 | 
174 | **Parameters:**
175 | 
176 | - `byteOffset`: The offset within the view.
177 | - `littleEndian`: Optional. Default is false. Use true if the number is stored in little-endian format.
178 | 
179 | ---
180 | 
181 | ### getInt32
182 | 
183 | **Signature:** `getInt32(byteOffset : Number, littleEndian : boolean) : Number`
184 | 
185 | **Description:** Returns the 32-bit signed integer number at the given offset.
186 | 
187 | **Parameters:**
188 | 
189 | - `byteOffset`: The offset within the view.
190 | - `littleEndian`: Optional. Default is false. Use true if the number is stored in little-endian format.
191 | 
192 | ---
193 | 
194 | ### getInt8
195 | 
196 | **Signature:** `getInt8(byteOffset : Number) : Number`
197 | 
198 | **Description:** Returns the 8-bit signed integer number at the given offset.
199 | 
200 | **Parameters:**
201 | 
202 | - `byteOffset`: The offset within the view.
203 | 
204 | ---
205 | 
206 | ### getUint16
207 | 
208 | **Signature:** `getUint16(byteOffset : Number, littleEndian : boolean) : Number`
209 | 
210 | **Description:** Returns the 16-bit unsigned integer number at the given offset.
211 | 
212 | **Parameters:**
213 | 
214 | - `byteOffset`: The offset within the view.
215 | - `littleEndian`: Optional. Default is false. Use true if the number is stored in little-endian format.
216 | 
217 | ---
218 | 
219 | ### getUint32
220 | 
221 | **Signature:** `getUint32(byteOffset : Number, littleEndian : boolean) : Number`
222 | 
223 | **Description:** Returns the 32-bit unsigned integer number at the given offset.
224 | 
225 | **Parameters:**
226 | 
227 | - `byteOffset`: The offset within the view.
228 | - `littleEndian`: Optional. Default is false. Use true if the number is stored in little-endian format.
229 | 
230 | ---
231 | 
232 | ### getUint8
233 | 
234 | **Signature:** `getUint8(byteOffset : Number) : Number`
235 | 
236 | **Description:** Returns the 8-bit unsigned integer number at the given offset.
237 | 
238 | **Parameters:**
239 | 
240 | - `byteOffset`: The offset within the view.
241 | 
242 | ---
243 | 
244 | ### setFloat32
245 | 
246 | **Signature:** `setFloat32(byteOffset : Number, value : Number, littleEndian : boolean) : void`
247 | 
248 | **Description:** Writes a 32-bit floating point number into the byte array at the given offset.
249 | 
250 | **Parameters:**
251 | 
252 | - `byteOffset`: The offset within the view.
253 | - `value`: The value to be written.
254 | - `littleEndian`: Optional. Default is false. Use true if the little-endian format is to be used.
255 | 
256 | ---
257 | 
258 | ### setFloat64
259 | 
260 | **Signature:** `setFloat64(byteOffset : Number, value : Number, littleEndian : boolean) : void`
261 | 
262 | **Description:** Writes a 64-bit floating point number into the byte array at the given offset.
263 | 
264 | **Parameters:**
265 | 
266 | - `byteOffset`: The offset within the view.
267 | - `value`: The value to be written.
268 | - `littleEndian`: Optional. Default is false. Use true if the little-endian format is to be used.
269 | 
270 | ---
271 | 
272 | ### setInt16
273 | 
274 | **Signature:** `setInt16(byteOffset : Number, value : Number, littleEndian : boolean) : void`
275 | 
276 | **Description:** Writes a 16-bit signed integer number into the byte array at the given offset.
277 | 
278 | **Parameters:**
279 | 
280 | - `byteOffset`: The offset within the view.
281 | - `value`: The value to be written.
282 | - `littleEndian`: Optional. Default is false. Use true if the little-endian format is to be used.
283 | 
284 | ---
285 | 
286 | ### setInt32
287 | 
288 | **Signature:** `setInt32(byteOffset : Number, value : Number, littleEndian : boolean) : void`
289 | 
290 | **Description:** Writes a 32-bit signed integer number into the byte array at the given offset.
291 | 
292 | **Parameters:**
293 | 
294 | - `byteOffset`: The offset within the view.
295 | - `value`: The value to be written.
296 | - `littleEndian`: Optional. Default is false. Use true if the little-endian format is to be used.
297 | 
298 | ---
299 | 
300 | ### setInt8
301 | 
302 | **Signature:** `setInt8(byteOffset : Number, value : Number) : void`
303 | 
304 | **Description:** Writes an 8-bit signed integer number into the byte array at the given offset.
305 | 
306 | **Parameters:**
307 | 
308 | - `byteOffset`: The offset within the view.
309 | - `value`: The value to be written.
310 | 
311 | ---
312 | 
313 | ### setUint16
314 | 
315 | **Signature:** `setUint16(byteOffset : Number, value : Number, littleEndian : boolean) : void`
316 | 
317 | **Description:** Writes a 16-bit unsigned integer number into the byte array at the given offset.
318 | 
319 | **Parameters:**
320 | 
321 | - `byteOffset`: The offset within the view.
322 | - `value`: The value to be written.
323 | - `littleEndian`: Optional. Default is false. Use true if the little-endian format is to be used.
324 | 
325 | ---
326 | 
327 | ### setUint32
328 | 
329 | **Signature:** `setUint32(byteOffset : Number, value : Number, littleEndian : boolean) : void`
330 | 
331 | **Description:** Writes a 32-bit unsigned integer number into the byte array at the given offset.
332 | 
333 | **Parameters:**
334 | 
335 | - `byteOffset`: The offset within the view.
336 | - `value`: The value to be written.
337 | - `littleEndian`: Optional. Default is false. Use true if the little-endian format is to be used.
338 | 
339 | ---
340 | 
341 | ### setUint8
342 | 
343 | **Signature:** `setUint8(byteOffset : Number, value : Number) : void`
344 | 
345 | **Description:** Writes an 8-bit unsigned integer number into the byte array at the given offset.
346 | 
347 | **Parameters:**
348 | 
349 | - `byteOffset`: The offset within the view.
350 | - `value`: The value to be written.
351 | 
352 | ---
```

--------------------------------------------------------------------------------
/tests/client-factory.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { ClientFactory } from '../src/core/handlers/client-factory.js';
  2 | import { HandlerContext } from '../src/core/handlers/base-handler.js';
  3 | import { Logger } from '../src/utils/logger.js';
  4 | import { SFCCConfig } from '../src/types/types.js';
  5 | 
  6 | // Mock the clients
  7 | jest.mock('../src/clients/log-client.js');
  8 | jest.mock('../src/clients/ocapi-client.js');
  9 | jest.mock('../src/clients/ocapi/code-versions-client.js');
 10 | jest.mock('../src/clients/cartridge-generation-client.js');
 11 | 
 12 | describe('ClientFactory', () => {
 13 |   let mockLogger: jest.Mocked<Logger>;
 14 |   let factory: ClientFactory;
 15 | 
 16 |   beforeEach(() => {
 17 |     mockLogger = {
 18 |       debug: jest.fn(),
 19 |       info: jest.fn(),
 20 |       warn: jest.fn(),
 21 |       error: jest.fn(),
 22 |       timing: jest.fn(),
 23 |     } as any;
 24 |   });
 25 | 
 26 |   describe('createLogClient', () => {
 27 |     it('should create log client when capabilities and config are available', () => {
 28 |       const context: HandlerContext = {
 29 |         logger: mockLogger,
 30 |         config: { hostname: 'test.com' } as SFCCConfig,
 31 |         capabilities: {
 32 |           canAccessLogs: true,
 33 |           canAccessOCAPI: false,
 34 |         },
 35 |       };
 36 |       factory = new ClientFactory(context, mockLogger);
 37 | 
 38 |       const client = factory.createLogClient();
 39 | 
 40 |       expect(client).toBeDefined();
 41 |       expect(mockLogger.debug).toHaveBeenCalledWith('Creating SFCC Log Client');
 42 |     });
 43 | 
 44 |     it('should return null when log access capability is missing', () => {
 45 |       const context: HandlerContext = {
 46 |         logger: mockLogger,
 47 |         config: { hostname: 'test.com' } as SFCCConfig,
 48 |         capabilities: {
 49 |           canAccessLogs: false,
 50 |           canAccessOCAPI: false,
 51 |         },
 52 |       };
 53 |       factory = new ClientFactory(context, mockLogger);
 54 | 
 55 |       const client = factory.createLogClient();
 56 | 
 57 |       expect(client).toBeNull();
 58 |       expect(mockLogger.debug).toHaveBeenCalledWith('Log client not created: missing log access capability or config');
 59 |     });
 60 | 
 61 |     it('should return null when config is missing', () => {
 62 |       const context: HandlerContext = {
 63 |         logger: mockLogger,
 64 |         config: undefined as any,
 65 |         capabilities: {
 66 |           canAccessLogs: true,
 67 |           canAccessOCAPI: false,
 68 |         },
 69 |       };
 70 |       factory = new ClientFactory(context, mockLogger);
 71 | 
 72 |       const client = factory.createLogClient();
 73 | 
 74 |       expect(client).toBeNull();
 75 |       expect(mockLogger.debug).toHaveBeenCalledWith('Log client not created: missing log access capability or config');
 76 |     });
 77 |   });
 78 | 
 79 |   describe('createOCAPIClient', () => {
 80 |     it('should create OCAPI client when all credentials are available', () => {
 81 |       const context: HandlerContext = {
 82 |         logger: mockLogger,
 83 |         config: {
 84 |           hostname: 'test.com',
 85 |           clientId: 'client123',
 86 |           clientSecret: 'secret123',
 87 |         } as SFCCConfig,
 88 |         capabilities: {
 89 |           canAccessLogs: false,
 90 |           canAccessOCAPI: true,
 91 |         },
 92 |       };
 93 |       factory = new ClientFactory(context, mockLogger);
 94 | 
 95 |       const client = factory.createOCAPIClient();
 96 | 
 97 |       expect(client).toBeDefined();
 98 |       expect(mockLogger.debug).toHaveBeenCalledWith('Creating OCAPI Client');
 99 |     });
100 | 
101 |     it('should return null when OCAPI capability is missing', () => {
102 |       const context: HandlerContext = {
103 |         logger: mockLogger,
104 |         config: {
105 |           hostname: 'test.com',
106 |           clientId: 'client123',
107 |           clientSecret: 'secret123',
108 |         } as SFCCConfig,
109 |         capabilities: {
110 |           canAccessLogs: false,
111 |           canAccessOCAPI: false,
112 |         },
113 |       };
114 |       factory = new ClientFactory(context, mockLogger);
115 | 
116 |       const client = factory.createOCAPIClient();
117 | 
118 |       expect(client).toBeNull();
119 |       expect(mockLogger.debug).toHaveBeenCalledWith('OCAPI client not created: missing OCAPI credentials or capability');
120 |     });
121 | 
122 |     it('should return null when hostname is missing', () => {
123 |       const context: HandlerContext = {
124 |         logger: mockLogger,
125 |         config: {
126 |           clientId: 'client123',
127 |           clientSecret: 'secret123',
128 |         } as SFCCConfig,
129 |         capabilities: {
130 |           canAccessLogs: false,
131 |           canAccessOCAPI: true,
132 |         },
133 |       };
134 |       factory = new ClientFactory(context, mockLogger);
135 | 
136 |       const client = factory.createOCAPIClient();
137 | 
138 |       expect(client).toBeNull();
139 |       expect(mockLogger.debug).toHaveBeenCalledWith('OCAPI client not created: missing OCAPI credentials or capability');
140 |     });
141 | 
142 |     it('should return null when clientId is missing', () => {
143 |       const context: HandlerContext = {
144 |         logger: mockLogger,
145 |         config: {
146 |           hostname: 'test.com',
147 |           clientSecret: 'secret123',
148 |         } as SFCCConfig,
149 |         capabilities: {
150 |           canAccessLogs: false,
151 |           canAccessOCAPI: true,
152 |         },
153 |       };
154 |       factory = new ClientFactory(context, mockLogger);
155 | 
156 |       const client = factory.createOCAPIClient();
157 | 
158 |       expect(client).toBeNull();
159 |       expect(mockLogger.debug).toHaveBeenCalledWith('OCAPI client not created: missing OCAPI credentials or capability');
160 |     });
161 | 
162 |     it('should return null when clientSecret is missing', () => {
163 |       const context: HandlerContext = {
164 |         logger: mockLogger,
165 |         config: {
166 |           hostname: 'test.com',
167 |           clientId: 'client123',
168 |         } as SFCCConfig,
169 |         capabilities: {
170 |           canAccessLogs: false,
171 |           canAccessOCAPI: true,
172 |         },
173 |       };
174 |       factory = new ClientFactory(context, mockLogger);
175 | 
176 |       const client = factory.createOCAPIClient();
177 | 
178 |       expect(client).toBeNull();
179 |       expect(mockLogger.debug).toHaveBeenCalledWith('OCAPI client not created: missing OCAPI credentials or capability');
180 |     });
181 |   });
182 | 
183 |   describe('createCodeVersionsClient', () => {
184 |     it('should create code versions client when all credentials are available', () => {
185 |       const context: HandlerContext = {
186 |         logger: mockLogger,
187 |         config: {
188 |           hostname: 'test.com',
189 |           clientId: 'client123',
190 |           clientSecret: 'secret123',
191 |         } as SFCCConfig,
192 |         capabilities: {
193 |           canAccessLogs: false,
194 |           canAccessOCAPI: true,
195 |         },
196 |       };
197 |       factory = new ClientFactory(context, mockLogger);
198 | 
199 |       const client = factory.createCodeVersionsClient();
200 | 
201 |       expect(client).toBeDefined();
202 |       expect(mockLogger.debug).toHaveBeenCalledWith('Creating OCAPI Code Versions Client');
203 |     });
204 | 
205 |     it('should return null when credentials are missing', () => {
206 |       const context: HandlerContext = {
207 |         logger: mockLogger,
208 |         config: {
209 |           hostname: 'test.com',
210 |         } as SFCCConfig,
211 |         capabilities: {
212 |           canAccessLogs: false,
213 |           canAccessOCAPI: true,
214 |         },
215 |       };
216 |       factory = new ClientFactory(context, mockLogger);
217 | 
218 |       const client = factory.createCodeVersionsClient();
219 | 
220 |       expect(client).toBeNull();
221 |       expect(mockLogger.debug).toHaveBeenCalledWith('Code versions client not created: missing OCAPI credentials or capability');
222 |     });
223 |   });
224 | 
225 |   describe('createCartridgeClient', () => {
226 |     it('should create cartridge client with default services', () => {
227 |       const context: HandlerContext = {
228 |         logger: mockLogger,
229 |         config: {} as SFCCConfig,
230 |         capabilities: {
231 |           canAccessLogs: false,
232 |           canAccessOCAPI: false,
233 |         },
234 |       };
235 |       factory = new ClientFactory(context, mockLogger);
236 | 
237 |       const client = factory.createCartridgeClient();
238 | 
239 |       expect(client).toBeDefined();
240 |       expect(mockLogger.debug).toHaveBeenCalledWith('Creating Cartridge Generation Client');
241 |     });
242 | 
243 |     it('should create cartridge client with custom services', () => {
244 |       const context: HandlerContext = {
245 |         logger: mockLogger,
246 |         config: {} as SFCCConfig,
247 |         capabilities: {
248 |           canAccessLogs: false,
249 |           canAccessOCAPI: false,
250 |         },
251 |       };
252 |       factory = new ClientFactory(context, mockLogger);
253 | 
254 |       const mockFileSystem = { writeFile: jest.fn() } as any;
255 |       const mockPath = { join: jest.fn() } as any;
256 | 
257 |       const client = factory.createCartridgeClient(mockFileSystem, mockPath);
258 | 
259 |       expect(client).toBeDefined();
260 |       expect(mockLogger.debug).toHaveBeenCalledWith('Creating Cartridge Generation Client');
261 |     });
262 |   });
263 | 
264 |   describe('getClientRequiredError', () => {
265 |     it('should return OCAPI error message', () => {
266 |       const error = ClientFactory.getClientRequiredError('OCAPI');
267 |       expect(error).toBe('OCAPI client not configured - ensure credentials are provided in full mode.');
268 |     });
269 | 
270 |     it('should return Log error message', () => {
271 |       const error = ClientFactory.getClientRequiredError('Log');
272 |       expect(error).toBe('Log client not configured - ensure log access is enabled.');
273 |     });
274 | 
275 |     it('should return default error message for unknown client type', () => {
276 |       const error = ClientFactory.getClientRequiredError('Unknown' as any);
277 |       expect(error).toBe('Required client not configured.');
278 |     });
279 |   });
280 | });
281 | 
```

--------------------------------------------------------------------------------
/tests/mcp/node/get-hook-reference.docs-only.programmatic.test.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 |  * Programmatic tests for get_hook_reference tool (docs-only mode)
  3 |  *
  4 |  * Response formats discovered via aegis query:
  5 |  *  Success (ocapi_hooks): { content:[{type:'text', text:'[ {"category": ... } ]'}], isError:false }
  6 |  *  Success (scapi_hooks): similar but includes signature fields in JSON text
  7 |  *  Invalid guideName: content text is "[]" (empty JSON array), isError:false
  8 |  *  Validation error (missing/empty guideName): { content:[{text:'Error: guideName must be a non-empty string'}], isError:true }
  9 |  *
 10 |  * This suite validates:
 11 |  *  - Tool presence in listTools
 12 |  *  - Successful retrieval for ocapi_hooks & scapi_hooks
 13 |  *  - Structural JSON parsing of categories & hooks
 14 |  *  - Presence of multiple hookPoints for basket endpoints
 15 |  *  - Presence of hook signatures in scapi_hooks
 16 |  *  - Validation errors for missing & empty guideName
 17 |  *  - Empty result handling for invalid guideName
 18 |  */
 19 | 
 20 | import { describe, test, before, after, beforeEach } from 'node:test';
 21 | import { strict as assert } from 'node:assert';
 22 | import { connect } from 'mcp-aegis';
 23 | 
 24 | 
 25 | function parseHookReference(result) {
 26 |   assert.equal(result.isError, false, 'Expected non-error result');
 27 |   assert.ok(Array.isArray(result.content), 'content must be array');
 28 |   assert.ok(result.content.length > 0, 'content should have at least one item');
 29 |   const textItem = result.content.find(i => i.type === 'text');
 30 |   assert.ok(textItem, 'text content item required');
 31 |   const raw = textItem.text.trim();
 32 |   let data;
 33 |   // eslint-disable-next-line @typescript-eslint/no-unused-vars
 34 |   try { data = JSON.parse(raw); } catch (e) {
 35 |     throw new Error('Failed to JSON.parse hook reference payload: ' + raw.slice(0,200));
 36 |   }
 37 |   assert.ok(Array.isArray(data), 'Top-level parsed data must be array of categories');
 38 |   return { raw, data };
 39 | }
 40 | 
 41 | function findCategory(data, nameFragment) {
 42 |   return data.find(c => typeof c.category === 'string' && c.category.toLowerCase().includes(nameFragment.toLowerCase()));
 43 | }
 44 | 
 45 | function validateHookSchema(h) {
 46 |   assert.ok(typeof h.endpoint === 'string' && h.endpoint.length > 0, 'hook.endpoint must be non-empty string');
 47 |   assert.ok(Array.isArray(h.hookPoints), 'hook.hookPoints must be array');
 48 |   h.hookPoints.forEach(p => assert.ok(typeof p === 'string' && p.length > 0, 'hookPoint must be non-empty string'));
 49 |   if ('signature' in h) {
 50 |     assert.ok(typeof h.signature === 'string' && /\w+\(/.test(h.signature), 'signature should look like a function');
 51 |   }
 52 | }
 53 | 
 54 | describe('get_hook_reference.docs-only (programmatic)', () => {
 55 |   let client;
 56 | 
 57 |   before(async () => {
 58 |     client = await connect('./aegis.config.docs-only.json');
 59 |   });
 60 | 
 61 |   after(async () => {
 62 |     if (client?.connected) await client.disconnect();
 63 |   });
 64 | 
 65 |   beforeEach(() => {
 66 |     client.clearAllBuffers(); // Recommended - comprehensive protection
 67 |   });
 68 | 
 69 |   test('tool should be present in listTools', async () => {
 70 |     const tools = await client.listTools();
 71 |     const names = tools.map(t => t.name);
 72 |     assert.ok(names.includes('get_hook_reference'), 'get_hook_reference should be registered');
 73 |   });
 74 | 
 75 |   test('ocapi_hooks reference basic structure', async () => {
 76 |     const result = await client.callTool('get_hook_reference', { guideName: 'ocapi_hooks' });
 77 |     const { data, raw } = parseHookReference(result);
 78 |     assert.ok(data.length >= 2, 'Expect >=2 categories (Shop API Hooks, Data API Hooks)');
 79 | 
 80 |     const shopCat = findCategory(data, 'shop api');
 81 |     const dataCat = findCategory(data, 'data api');
 82 |     assert.ok(shopCat, 'Shop API Hooks category missing');
 83 |     assert.ok(dataCat, 'Data API Hooks category missing');
 84 | 
 85 |     // Validate hook objects minimally
 86 |     for (const cat of [shopCat, dataCat]) {
 87 |       assert.ok(Array.isArray(cat.hooks), 'category.hooks must be array');
 88 |       assert.ok(cat.hooks.length > 0, 'category.hooks should not be empty');
 89 |       const firstHook = cat.hooks[0];
 90 |       assert.ok(firstHook.endpoint, 'hook.endpoint required');
 91 |       assert.ok(Array.isArray(firstHook.hookPoints), 'hook.hookPoints must be array');
 92 |     }
 93 | 
 94 |     // Basket POST should have multiple hookPoints including modifyPOSTResponse or validateBasket
 95 |     const basketPost = shopCat.hooks.find(h => /POST \/baskets$/.test(h.endpoint));
 96 |     if (basketPost) {
 97 |       const points = basketPost.hookPoints.join(' ');
 98 |       assert.ok(/modifyPOSTResponse/.test(points) || /validateBasket/.test(points), 'Basket POST should expose modifyPOSTResponse or validateBasket');
 99 |     } else {
100 |       // Non-fatal, but log for debugging
101 |       console.warn('Basket POST endpoint not found in OCAPI shop hooks');
102 |     }
103 | 
104 |     assert.ok(raw.length > 500, 'Raw OCAPI JSON text should exceed 500 chars for richness');
105 |   });
106 | 
107 |   test('scapi_hooks reference includes signatures & structural integrity', async () => {
108 |     const result = await client.callTool('get_hook_reference', { guideName: 'scapi_hooks' });
109 |     const { data, raw } = parseHookReference(result);
110 |     assert.ok(data.length >= 3, 'Expect >=3 categories for SCAPI');
111 | 
112 |     const basketCat = findCategory(data, 'baskets');
113 |     const ordersCat = findCategory(data, 'orders');
114 |     assert.ok(basketCat, 'Basket category missing');
115 |     assert.ok(ordersCat, 'Orders category missing');
116 | 
117 |     // Validate full schema for first 3 hooks of basket category (or all if <3)
118 |     basketCat.hooks.slice(0,3).forEach(validateHookSchema);
119 | 
120 |     // SCAPI version adds signature field per hook (at least some hooks)
121 |     const signaturePresent = basketCat.hooks.some(h => 'signature' in h && /\w+\(.+\)/.test(h.signature));
122 |     assert.ok(signaturePresent, 'Expected at least one signature field with function pattern in SCAPI hooks');
123 | 
124 |     // Validate at least one hook shows beforePOST/afterPOST pair
125 |     const anyBefore = basketCat.hooks.some(h => h.hookPoints.some(p => /beforePOST/.test(p)));
126 |     const anyAfter = basketCat.hooks.some(h => h.hookPoints.some(p => /afterPOST/.test(p)));
127 |     assert.ok(anyBefore && anyAfter, 'Expect beforePOST and afterPOST patterns in SCAPI baskets');
128 | 
129 |     // Endpoint uniqueness across all categories
130 |     const allEndpoints = data.flatMap(c => c.hooks.map(h => h.endpoint));
131 |     const uniqueCount = new Set(allEndpoints).size;
132 |     // Relaxed: just ensure we have a reasonable number of endpoints and log uniqueness ratio for diagnostic purposes
133 |     assert.ok(allEndpoints.length > 10, 'Should expose more than 10 endpoints total');
134 |     const uniquenessRatio = uniqueCount / allEndpoints.length;
135 |     assert.ok(uniquenessRatio > 0.2, 'Uniqueness ratio should be >20% (diagnostic sanity check)');
136 |     // console.debug(`Endpoint uniqueness ratio: ${(uniquenessRatio*100).toFixed(1)}%`);
137 | 
138 |     assert.ok(raw.length > 800, 'Raw SCAPI JSON text should exceed 800 chars for richness');
139 |   });
140 | 
141 |   // Additional targeted signature regex test
142 |   test('SCAPI signatures follow expected function pattern', async () => {
143 |     const result = await client.callTool('get_hook_reference', { guideName: 'scapi_hooks' });
144 |     const { data } = parseHookReference(result);
145 |     const signatures = data.flatMap(c => c.hooks.filter(h => h.signature).map(h => h.signature));
146 |     assert.ok(signatures.length > 5, 'Expect multiple signatures');
147 |     // All signatures should have pattern name(args) : returnType
148 |     const invalid = signatures.filter(sig => !/^\w+\([^)]*\)\s*:\s*\w+\.?\w*/.test(sig));
149 |     assert.ok(invalid.length === 0, 'All signatures should match basic function signature pattern');
150 |   });
151 | 
152 |   test('OCAPI hook objects basic schema validation', async () => {
153 |     const result = await client.callTool('get_hook_reference', { guideName: 'ocapi_hooks' });
154 |     const { data } = parseHookReference(result);
155 |     data.forEach(cat => {
156 |       cat.hooks.slice(0,5).forEach(h => {
157 |         validateHookSchema(h);
158 |         assert.ok(!('signature' in h) || typeof h.signature === 'string', 'signature optional but must be string if present');
159 |       });
160 |     });
161 |   });
162 | 
163 |   test('invalid guideName returns empty array string (non-error)', async () => {
164 |     const result = await client.callTool('get_hook_reference', { guideName: 'invalid_hooks' });
165 |     assert.equal(result.isError, false, 'invalid guide should not set isError');
166 |     const textItem = result.content.find(i => i.type === 'text');
167 |     assert.ok(textItem, 'text item required');
168 |     assert.equal(textItem.text.trim(), '[]', 'Expected empty JSON array payload');
169 |   });
170 | 
171 |   test('missing guideName validation error', async () => {
172 |     const result = await client.callTool('get_hook_reference', {}); // missing param
173 |     assert.equal(result.isError, true, 'Should be error');
174 |     const msg = result.content[0].text;
175 |     assert.ok(/guideName must be a non-empty string/.test(msg), 'Validation message missing');
176 |   });
177 | 
178 |   test('empty guideName validation error', async () => {
179 |     const result = await client.callTool('get_hook_reference', { guideName: '' });
180 |     assert.equal(result.isError, true, 'Should be error');
181 |     const msg = result.content[0].text;
182 |     assert.ok(/guideName must be a non-empty string/.test(msg), 'Validation message missing');
183 |   });
184 | 
185 | });
186 | 
```

--------------------------------------------------------------------------------
/docs/dw_extensions.paymentrequest/PaymentRequestHooks.md:
--------------------------------------------------------------------------------

```markdown
  1 | ## Package: dw.extensions.paymentrequest
  2 | 
  3 | # Class PaymentRequestHooks
  4 | 
  5 | ## Inheritance Hierarchy
  6 | 
  7 | - dw.extensions.paymentrequest.PaymentRequestHooks
  8 | 
  9 | ## Description
 10 | 
 11 | PaymentRequestHooks interface containing extension points for customizing Payment Requests. These hooks are executed in a transaction. The extension points (hook names), and the functions that are called by each extension point. A function must be defined inside a JavaScript source and must be exported. The script with the exported hook function must be located inside a site cartridge. Inside the site cartridge a 'package.json' file with a 'hooks' entry must exist. "hooks": "./hooks.json" The hooks entry links to a json file, relative to the 'package.json' file. This file lists all registered hooks inside the hooks property: "hooks": [ {"name": "dw.extensions.paymentrequest.getPaymentRequest", "script": "./paymentrequest.ds"} {"name": "dw.extensions.paymentrequest.shippingAddressChange", "script": "./paymentrequest.ds"} ] A hook entry has a 'name' and a 'script' property. The 'name' contains the extension point, the hook name. The 'script' contains the script relative to the hooks file, with the exported hook function.
 12 | 
 13 | ## Constants
 14 | 
 15 | ## Properties
 16 | 
 17 | ## Constructor Summary
 18 | 
 19 | ## Method Summary
 20 | 
 21 | ### abort
 22 | 
 23 | **Signature:** `abort(basket : Basket) : PaymentRequestHookResult`
 24 | 
 25 | Called after the Payment Request user interface was canceled.
 26 | 
 27 | ### authorizeOrderPayment
 28 | 
 29 | **Signature:** `authorizeOrderPayment(order : Order, response : Object) : Status`
 30 | 
 31 | Called after the shopper accepts the Payment Request payment for the given order.
 32 | 
 33 | ### getPaymentRequest
 34 | 
 35 | **Signature:** `getPaymentRequest(basket : Basket, parameters : Object) : PaymentRequestHookResult`
 36 | 
 37 | Called to get the PaymentRequest constructor parameters for the given basket.
 38 | 
 39 | ### placeOrder
 40 | 
 41 | **Signature:** `placeOrder(order : Order) : PaymentRequestHookResult`
 42 | 
 43 | Called after payment has been authorized and the given Payment Request order is ready to be placed.
 44 | 
 45 | ### shippingAddressChange
 46 | 
 47 | **Signature:** `shippingAddressChange(basket : Basket, details : Object) : PaymentRequestHookResult`
 48 | 
 49 | Called after handling the Payment Request shippingaddresschange event for the given basket.
 50 | 
 51 | ### shippingOptionChange
 52 | 
 53 | **Signature:** `shippingOptionChange(basket : Basket, shippingMethod : ShippingMethod, details : Object) : PaymentRequestHookResult`
 54 | 
 55 | Called after handling the Payment Request shippingoptionchange event for the given basket.
 56 | 
 57 | ## Method Detail
 58 | 
 59 | ## Method Details
 60 | 
 61 | ### abort
 62 | 
 63 | **Signature:** `abort(basket : Basket) : PaymentRequestHookResult`
 64 | 
 65 | **Description:** Called after the Payment Request user interface was canceled. The given basket is the one that was passed to other hooks earlier in the Payment Request checkout process. It is not guaranteed that this hook will be executed for all Payment Request user interfaces canceled by shoppers or otherwise ended without a successful order. Calls to this hook are provided on a best-effort basis. If the returned result includes a redirect URL, the shopper browser will be navigated to that URL if possible. It is not guaranteed that the response with the hook result will be handled in the shopper browser in all cases.
 66 | 
 67 | **Parameters:**
 68 | 
 69 | - `basket`: the basket that was being checked out using Payment Request
 70 | 
 71 | **Returns:**
 72 | 
 73 | a non-null result ends the hook execution
 74 | 
 75 | ---
 76 | 
 77 | ### authorizeOrderPayment
 78 | 
 79 | **Signature:** `authorizeOrderPayment(order : Order, response : Object) : Status`
 80 | 
 81 | **Description:** Called after the shopper accepts the Payment Request payment for the given order. Basket customer information, billing address, and/or shipping address for the default shipment will have already been updated to reflect the available contact information provided by Payment Request. Any preexisting payment instruments on the basket will have been removed, and a single DW_ANDROID_PAY payment instrument added for the total amount. The given order will have been created from this updated basket. The purpose of this hook is to authorize the Payment Request payment for the order. If a non-error status is returned that means that you have successfully authorized the payment with your payment service provider. Your hook implementation must set the necessary payment status and transaction identifier data on the order as returned by the provider. Return an error status to indicate a problem, including unsuccessful authorization. See the Payment Request API for more information.
 82 | 
 83 | **Parameters:**
 84 | 
 85 | - `order`: the order paid using Payment Request
 86 | - `response`: response to the accepted PaymentRequest
 87 | 
 88 | **Returns:**
 89 | 
 90 | a non-null status ends the hook execution
 91 | 
 92 | ---
 93 | 
 94 | ### getPaymentRequest
 95 | 
 96 | **Signature:** `getPaymentRequest(basket : Basket, parameters : Object) : PaymentRequestHookResult`
 97 | 
 98 | **Description:** Called to get the PaymentRequest constructor parameters for the given basket. You can set properties in the given parameters object to extend or override default properties set automatically based on the Google Pay configuration for your site. The parameters object will contain the following properties by default: methodData - array containing payment methods the web site accepts details - information about the transaction that the user is being asked to complete options - information about what options the web page wishes to use from the payment request system Return a result with an error status to indicate a problem. If the returned result includes a redirect URL, the shopper browser will be navigated to that URL if the Payment Request user interaction is canceled. See the Payment Request API for more information.
 99 | 
100 | **Parameters:**
101 | 
102 | - `basket`: the basket for the Payment Request request
103 | - `parameters`: object containing PaymentRequest constructor parameters
104 | 
105 | **Returns:**
106 | 
107 | a non-null result ends the hook execution
108 | 
109 | ---
110 | 
111 | ### placeOrder
112 | 
113 | **Signature:** `placeOrder(order : Order) : PaymentRequestHookResult`
114 | 
115 | **Description:** Called after payment has been authorized and the given Payment Request order is ready to be placed. The purpose of this hook is to place the order, or return a redirect URL that results in the order being placed when the shopper browser is navigated to it. The default implementation of this hook returns a redirect to COPlaceOrder-Submit with URL parameters order_id set to Order.getOrderNo() and order_token set to Order.getOrderToken() which corresponds to SiteGenesis-based implementations. Your hook implementation should return a result with a different redirect URL as necessary to place the order and show an order confirmation. Alternatively, your hook implementation itself can place the order and return a result with a redirect URL to an order confirmation page that does not place the order. This is inconsistent with SiteGenesis-based implementations so is not the default. Return an error status to indicate a problem. If the returned result includes a redirect URL, the shopper browser will be navigated to that URL if the Payment Request user interface is canceled.
116 | 
117 | **Parameters:**
118 | 
119 | - `order`: the order paid using PaymentRequest
120 | 
121 | **Returns:**
122 | 
123 | a non-null result ends the hook execution
124 | 
125 | ---
126 | 
127 | ### shippingAddressChange
128 | 
129 | **Signature:** `shippingAddressChange(basket : Basket, details : Object) : PaymentRequestHookResult`
130 | 
131 | **Description:** Called after handling the Payment Request shippingaddresschange event for the given basket. Basket customer information and/or shipping address for the default shipment will have already been updated to reflect the available shipping address information provided by Payment Request. The basket will have already been calculated before this hook is called. Return a result with an error status to indicate a problem. If the returned result includes a redirect URL, the shopper browser will be navigated to that URL if the Payment Request user interface is canceled. See the Payment Request API for more information.
132 | 
133 | **Parameters:**
134 | 
135 | - `basket`: the basket being checked out using Payment Request
136 | - `details`: updated PaymentRequest object details
137 | 
138 | **Returns:**
139 | 
140 | a non-null result ends the hook execution
141 | 
142 | ---
143 | 
144 | ### shippingOptionChange
145 | 
146 | **Signature:** `shippingOptionChange(basket : Basket, shippingMethod : ShippingMethod, details : Object) : PaymentRequestHookResult`
147 | 
148 | **Description:** Called after handling the Payment Request shippingoptionchange event for the given basket. The given shipping method will have already been set on the basket. The basket will have already been calculated before this hook is called. Return a result with an error status to indicate a problem. If the returned result includes a redirect URL, the shopper browser will be navigated to that URL if the Payment Request user interface is canceled. See the Payment Request API for more information.
149 | 
150 | **Parameters:**
151 | 
152 | - `basket`: the basket being checked out using Payment Request
153 | - `shippingMethod`: the shipping method that was selected
154 | - `details`: updated PaymentRequest object details
155 | 
156 | **Returns:**
157 | 
158 | a non-null result ends the hook execution
159 | 
160 | ---
```

--------------------------------------------------------------------------------
/tests/mcp/yaml/search-system-object-attribute-groups.docs-only.test.mcp.yml:
--------------------------------------------------------------------------------

```yaml
  1 | # ==================================================================================
  2 | # SFCC MCP Server - search_system_object_attribute_groups Tool YAML Tests (Docs-Only Mode)
  3 | # Tests that system object attribute group tools are NOT available in docs-only mode
  4 | # This tool requires SFCC credentials and should not be available without them
  5 | # However, the tool can still be called and should return authentication error
  6 | # 
  7 | # Quick Test Commands:
  8 | # aegis "tests/mcp/yaml/search-system-object-attribute-groups.docs-only.test.mcp.yml" --config "aegis.config.docs-only.json" --verbose
  9 | # aegis "tests/mcp/yaml/search-system-object-attribute-groups.docs-only.test.mcp.yml" --config "aegis.config.docs-only.json" --debug --timing
 10 | # aegis query --config "aegis.config.docs-only.json"
 11 | # aegis query search_system_object_attribute_groups '{"objectType": "Product", "searchRequest": {"query": {"match_all_query": {}}, "count": 3}}' --config "aegis.config.docs-only.json"
 12 | # ==================================================================================
 13 | 
 14 | description: "search_system_object_attribute_groups tool tests - Docs-only mode tool availability and authentication errors"
 15 | 
 16 | # ==================================================================================
 17 | # TOOL UNAVAILABILITY IN DOCS-ONLY MODE
 18 | # ==================================================================================
 19 | tests:
 20 |   - it: "should NOT list search_system_object_attribute_groups tool in docs-only mode"
 21 |     request:
 22 |       jsonrpc: "2.0"
 23 |       id: "tool-not-available-docs"
 24 |       method: "tools/list"
 25 |       params: {}
 26 |     expect:
 27 |       response:
 28 |         jsonrpc: "2.0"
 29 |         id: "tool-not-available-docs"
 30 |         result:
 31 |           match:extractField: "tools.*.name"
 32 |           value: "match:not:arrayContains:search_system_object_attribute_groups"
 33 |       stderr: "toBeEmpty"
 34 | 
 35 | # ==================================================================================
 36 | # AUTHENTICATION ERROR TESTS (Tool Can Be Called But Returns Error)
 37 | # ==================================================================================
 38 | 
 39 |   - it: "should return authentication error when calling search_system_object_attribute_groups in docs-only mode"
 40 |     request:
 41 |       jsonrpc: "2.0"
 42 |       id: "auth-error-product"
 43 |       method: "tools/call"
 44 |       params:
 45 |         name: "search_system_object_attribute_groups"
 46 |         arguments:
 47 |           objectType: "Product"
 48 |           searchRequest:
 49 |             query:
 50 |               match_all_query: {}
 51 |             count: 3
 52 |     expect:
 53 |       response:
 54 |         jsonrpc: "2.0"
 55 |         id: "auth-error-product"
 56 |         result:
 57 |           content:
 58 |             - type: "text"
 59 |               text: "match:contains:OCAPI client not configured"
 60 |           isError: true
 61 |       performance:
 62 |         maxResponseTime: "500ms"
 63 |       stderr: "toBeEmpty"
 64 | 
 65 |   - it: "should return authentication error for SitePreferences object type"
 66 |     request:
 67 |       jsonrpc: "2.0"
 68 |       id: "auth-error-siteprefs"
 69 |       method: "tools/call"
 70 |       params:
 71 |         name: "search_system_object_attribute_groups"
 72 |         arguments:
 73 |           objectType: "SitePreferences"
 74 |           searchRequest:
 75 |             query:
 76 |               match_all_query: {}
 77 |             count: 2
 78 |     expect:
 79 |       response:
 80 |         jsonrpc: "2.0"
 81 |         id: "auth-error-siteprefs"
 82 |         result:
 83 |           content:
 84 |             - type: "text"
 85 |               text: "match:contains:credentials are provided"
 86 |           isError: true
 87 |       stderr: "toBeEmpty"
 88 | 
 89 |   - it: "should return authentication error for Customer object type"
 90 |     request:
 91 |       jsonrpc: "2.0"
 92 |       id: "auth-error-customer"
 93 |       method: "tools/call"
 94 |       params:
 95 |         name: "search_system_object_attribute_groups"
 96 |         arguments:
 97 |           objectType: "Customer"
 98 |           searchRequest:
 99 |             query:
100 |               text_query:
101 |                 fields: ["id"]
102 |                 search_phrase: "PersonalInfo"
103 |             count: 5
104 |     expect:
105 |       response:
106 |         jsonrpc: "2.0"
107 |         id: "auth-error-customer"
108 |         result:
109 |           content:
110 |             - type: "text"
111 |               text: "match:contains:full mode"
112 |           isError: true
113 |       stderr: "toBeEmpty"
114 | 
115 |   - it: "should return authentication error for any object type with complex query"
116 |     request:
117 |       jsonrpc: "2.0"
118 |       id: "auth-error-complex"
119 |       method: "tools/call"
120 |       params:
121 |         name: "search_system_object_attribute_groups"
122 |         arguments:
123 |           objectType: "Order"
124 |           searchRequest:
125 |             query:
126 |               bool_query:
127 |                 must:
128 |                   - text_query:
129 |                       fields: ["id", "display_name"]
130 |                       search_phrase: "Billing"
131 |             sorts:
132 |               - field: "id"
133 |                 sort_order: "asc"
134 |             count: 3
135 |     expect:
136 |       response:
137 |         jsonrpc: "2.0"
138 |         id: "auth-error-complex"
139 |         result:
140 |           content:
141 |             - type: "text"
142 |               text: "match:contains:OCAPI client not configured"
143 |           isError: true
144 |       stderr: "toBeEmpty"
145 | 
146 |   # ==================================================================================
147 |   # BEHAVIOR VALIDATION IN DOCS-ONLY MODE
148 |   # ==================================================================================
149 | 
150 |   - it: "should return authentication error even for missing objectType parameter in docs-only mode"
151 |     request:
152 |       jsonrpc: "2.0"
153 |       id: "validation-missing-object-type"
154 |       method: "tools/call"
155 |       params:
156 |         name: "search_system_object_attribute_groups"
157 |         arguments:
158 |           searchRequest:
159 |             query:
160 |               match_all_query: {}
161 |             count: 3
162 |     expect:
163 |       response:
164 |         jsonrpc: "2.0"
165 |         id: "validation-missing-object-type"
166 |         result:
167 |           content:
168 |             - type: "text"
169 |               text: "match:contains:OCAPI client not configured"
170 |           isError: true
171 |       performance:
172 |         maxResponseTime: "500ms"
173 |       stderr: "toBeEmpty"
174 | 
175 |   - it: "should return authentication error even for empty objectType parameter in docs-only mode"
176 |     request:
177 |       jsonrpc: "2.0"
178 |       id: "validation-empty-object-type"
179 |       method: "tools/call"
180 |       params:
181 |         name: "search_system_object_attribute_groups"
182 |         arguments:
183 |           objectType: ""
184 |           searchRequest:
185 |             query:
186 |               match_all_query: {}
187 |             count: 2
188 |     expect:
189 |       response:
190 |         jsonrpc: "2.0"
191 |         id: "validation-empty-object-type"
192 |         result:
193 |           content:
194 |             - type: "text"
195 |               text: "match:contains:credentials are provided"
196 |           isError: true
197 |       performance:
198 |         maxResponseTime: "500ms"
199 |       stderr: "toBeEmpty"
200 | 
201 |   - it: "should return authentication error even for missing searchRequest parameter in docs-only mode"
202 |     request:
203 |       jsonrpc: "2.0"
204 |       id: "validation-missing-search-request"
205 |       method: "tools/call"
206 |       params:
207 |         name: "search_system_object_attribute_groups"
208 |         arguments:
209 |           objectType: "Product"
210 |     expect:
211 |       response:
212 |         jsonrpc: "2.0"
213 |         id: "validation-missing-search-request"
214 |         result:
215 |           content:
216 |             - type: "text"
217 |               text: "match:contains:full mode"
218 |           isError: true
219 |       performance:
220 |         maxResponseTime: "500ms"
221 |       stderr: "toBeEmpty"
222 | 
223 |   # ==================================================================================
224 |   # PERFORMANCE VALIDATION IN DOCS-ONLY MODE
225 |   # ==================================================================================
226 | 
227 |   - it: "should return authentication errors quickly in docs-only mode"
228 |     request:
229 |       jsonrpc: "2.0"
230 |       id: "performance-auth-error"
231 |       method: "tools/call"
232 |       params:
233 |         name: "search_system_object_attribute_groups"
234 |         arguments:
235 |           objectType: "Product"
236 |           searchRequest:
237 |             query:
238 |               match_all_query: {}
239 |             count: 10
240 |     expect:
241 |       response:
242 |         jsonrpc: "2.0"
243 |         id: "performance-auth-error"
244 |         result:
245 |           content:
246 |             - type: "text"
247 |               text: "match:contains:OCAPI client not configured"
248 |           isError: true
249 |       performance:
250 |         maxResponseTime: "400ms"
251 |       stderr: "toBeEmpty"
252 | 
253 |   - it: "should handle authentication errors quickly even with missing parameters in docs-only mode"
254 |     request:
255 |       jsonrpc: "2.0"
256 |       id: "performance-validation"
257 |       method: "tools/call"
258 |       params:
259 |         name: "search_system_object_attribute_groups"
260 |         arguments:
261 |           searchRequest:
262 |             query:
263 |               text_query:
264 |                 fields: ["id"]
265 |                 search_phrase: "test"
266 |             count: 5
267 |     expect:
268 |       response:
269 |         jsonrpc: "2.0"
270 |         id: "performance-validation"
271 |         result:
272 |           content:
273 |             - type: "text"
274 |               text: "match:contains:OCAPI client not configured"
275 |           isError: true
276 |       performance:
277 |         maxResponseTime: "300ms"
278 |       stderr: "toBeEmpty"
```

--------------------------------------------------------------------------------
/docs-site/components/Search.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import React, { useState, useEffect, useCallback, useRef } from 'react';
  2 | import { useLocation, useNavigate } from 'react-router-dom';
  3 | import { searchDocs, SearchResult } from '../utils/search';
  4 | import { SearchIcon } from './icons';
  5 | 
  6 | const Highlight: React.FC<{ text: string; query: string }> = ({ text, query }) => {
  7 |     if (!query) return <>{text}</>;
  8 |     const parts = text.split(new RegExp(`(${query})`, 'gi'));
  9 |     return (
 10 |         <>
 11 |             {parts.map((part, i) =>
 12 |                 part.toLowerCase() === query.toLowerCase() ? (
 13 |                     <mark key={i} className="bg-orange-200 text-orange-800 font-semibold rounded px-0.5">
 14 |                         {part}
 15 |                     </mark>
 16 |                 ) : (
 17 |                     part
 18 |                 )
 19 |             )}
 20 |         </>
 21 |     );
 22 | };
 23 | 
 24 | 
 25 | const Search: React.FC = () => {
 26 |     const [query, setQuery] = useState('');
 27 |     const [results, setResults] = useState<SearchResult[]>([]);
 28 |     const [isOpen, setIsOpen] = useState(false);
 29 |     const [activeIndex, setActiveIndex] = useState(-1);
 30 |     const inputRef = useRef<HTMLInputElement>(null);
 31 |     const resultsRef = useRef<HTMLUListElement>(null);
 32 |     const navigate = useNavigate();
 33 |     const location = useLocation();
 34 | 
 35 |     const openSearch = useCallback(() => {
 36 |         setIsOpen(true);
 37 |     }, []);
 38 | 
 39 |     const closeSearch = useCallback(() => {
 40 |         setIsOpen(false);
 41 |         setQuery('');
 42 |         setResults([]);
 43 |         setActiveIndex(-1);
 44 |     }, []);
 45 | 
 46 |     useEffect(() => {
 47 |         // Only run on client side
 48 |         if (typeof window === 'undefined') return;
 49 |         
 50 |         const handleKeyDown = (e: KeyboardEvent) => {
 51 |             if (e.metaKey && e.key === 'k') {
 52 |                 e.preventDefault();
 53 |                 if (isOpen) {
 54 |                     closeSearch();
 55 |                 } else {
 56 |                     openSearch();
 57 |                 }
 58 |             }
 59 | 
 60 |             if (isOpen) {
 61 |                 if (e.key === 'Escape') {
 62 |                     closeSearch();
 63 |                 } else if (e.key === 'ArrowDown') {
 64 |                     e.preventDefault();
 65 |                     setActiveIndex(prev => prev < results.length - 1 ? prev + 1 : prev);
 66 |                 } else if (e.key === 'ArrowUp') {
 67 |                     e.preventDefault();
 68 |                     setActiveIndex(prev => prev > 0 ? prev - 1 : prev);
 69 |                 } else if (e.key === 'Enter' && activeIndex >= 0) {
 70 |                     const result = results[activeIndex];
 71 |                     handleNavigation(result.path, result.heading, result.headingId);
 72 |                 }
 73 |             }
 74 |         };
 75 | 
 76 |         window.addEventListener('keydown', handleKeyDown);
 77 |         return () => window.removeEventListener('keydown', handleKeyDown);
 78 |     }, [isOpen, results.length, activeIndex, openSearch, closeSearch]);
 79 | 
 80 |     useEffect(() => {
 81 |         if (isOpen && inputRef.current) {
 82 |             inputRef.current.focus();
 83 |         }
 84 |     }, [isOpen]);
 85 | 
 86 |     useEffect(() => {
 87 |       closeSearch();
 88 |     }, [location.pathname, closeSearch]);
 89 | 
 90 |     useEffect(() => {
 91 |         if (activeIndex >= 0 && resultsRef.current) {
 92 |             const activeElement = resultsRef.current.children[activeIndex] as HTMLLIElement;
 93 |             if (activeElement) {
 94 |                 activeElement.scrollIntoView({ block: 'nearest' });
 95 |             }
 96 |         }
 97 |     }, [activeIndex]);
 98 | 
 99 |     const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
100 |         const newQuery = e.target.value;
101 |         setQuery(newQuery);
102 |         if (newQuery.length > 1) {
103 |             setResults(searchDocs(newQuery));
104 |         } else {
105 |             setResults([]);
106 |         }
107 |         setActiveIndex(0);
108 |     };
109 |     
110 |     const handleNavigation = (path: string, heading?: string, headingId?: string) => {
111 |         // Use the actual headingId if available, otherwise generate one from the heading
112 |         let targetPath = path;
113 |         let hashFragment = '';
114 |         
115 |         if (headingId) {
116 |             hashFragment = headingId;
117 |         } else if (heading && heading !== 'Introduction' && heading !== path.split('/').pop()) {
118 |             // Convert heading to a URL-safe ID as fallback
119 |             const generatedId = heading
120 |                 .toLowerCase()
121 |                 .replace(/[^a-z0-9\s-]/g, '') // Remove special characters except spaces and hyphens
122 |                 .replace(/\s+/g, '-') // Replace spaces with hyphens
123 |                 .replace(/-+/g, '-') // Replace multiple hyphens with single hyphen
124 |                 .replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
125 |             
126 |             if (generatedId) {
127 |                 hashFragment = generatedId;
128 |             }
129 |         }
130 |         
131 |         // Navigate to the path first, then handle hash navigation
132 |         if (hashFragment) {
133 |               navigate(targetPath + '#' + hashFragment);
134 |         } else {
135 |             navigate(targetPath);
136 |         }
137 |         
138 |         closeSearch();
139 |     };
140 | 
141 |     return (
142 |         <>
143 |             <div className="relative">
144 |                 <SearchIcon className="absolute top-1/2 left-3 -translate-y-1/2 w-4 h-4 text-slate-400 pointer-events-none" />
145 |                 <button
146 |                     type="button"
147 |                     onClick={openSearch}
148 |                     className="w-full bg-slate-100 border border-slate-200 rounded-lg py-2 pl-9 pr-12 sm:pr-3 text-sm text-left text-slate-500 hover:border-slate-300 transition-colors focus:outline-none focus:ring-2 focus:ring-orange-400"
149 |                 >
150 |                     Search...
151 |                 </button>
152 |                 <div className="absolute top-1/2 right-3 -translate-y-1/2 text-xs text-slate-400 border border-slate-300 rounded-md px-1.5 py-0.5 pointer-events-none hidden sm:block">
153 |                     ⌘K
154 |                 </div>
155 |             </div>
156 | 
157 |             {isOpen && (
158 |                  <div className="fixed inset-0 z-50 flex justify-center items-start pt-4 sm:pt-20 p-4" aria-modal="true">
159 |                     <div className="fixed inset-0 bg-slate-900/50 backdrop-blur-sm" onClick={closeSearch}></div>
160 |                     <div className="relative bg-white w-full max-w-2xl rounded-lg shadow-lg max-h-[90vh] flex flex-col">
161 |                         <div className="relative flex-shrink-0">
162 |                             <SearchIcon className="absolute top-1/2 left-4 -translate-y-1/2 w-5 h-5 text-slate-400" />
163 |                             <input
164 |                                 ref={inputRef}
165 |                                 type="text"
166 |                                 value={query}
167 |                                 onChange={handleSearch}
168 |                                 placeholder="Search documentation..."
169 |                                 className="w-full text-base sm:text-lg py-3 sm:py-4 pl-12 pr-4 border-b border-slate-200 focus:outline-none"
170 |                             />
171 |                         </div>
172 |                         {query.length > 1 && (
173 |                              <div className="flex-1 overflow-y-auto min-h-0">
174 |                                 {results.length > 0 ? (
175 |                                     <ul ref={resultsRef} className="p-3 sm:p-4 space-y-2">
176 |                                         {results.map((result, index) => (
177 |                                             <li key={`${result.path}-${result.heading}`}>
178 |                                                 <button
179 |                                                     onClick={() => handleNavigation(result.path, result.heading, result.headingId)}
180 |                                                     className={`w-full text-left p-3 rounded-md transition-colors ${activeIndex === index ? 'bg-orange-100' : 'hover:bg-slate-100'}`}
181 |                                                 >
182 |                                                     <div className="font-semibold text-slate-800 text-sm sm:text-base">
183 |                                                         <Highlight text={result.pageTitle} query={query} />
184 |                                                     </div>
185 |                                                     <div className="text-xs sm:text-sm text-slate-600 mb-1">
186 |                                                         <Highlight text={result.heading} query={query} />
187 |                                                     </div>
188 |                                                     <p className="text-xs sm:text-sm text-slate-500 line-clamp-2">
189 |                                                         <Highlight text={result.snippet} query={query} />
190 |                                                     </p>
191 |                                                 </button>
192 |                                             </li>
193 |                                         ))}
194 |                                     </ul>
195 |                                 ) : (
196 |                                     <p className="p-6 sm:p-8 text-center text-slate-500 text-sm sm:text-base">No results found for "{query}"</p>
197 |                                 )}
198 |                             </div>
199 |                         )}
200 |                     </div>
201 |                 </div>
202 |             )}
203 |         </>
204 |     );
205 | };
206 | 
207 | export default Search;
```

--------------------------------------------------------------------------------
/docs/sfra/response.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Class Response
  2 | 
  3 | ## Inheritance Hierarchy
  4 | 
  5 | - Object
  6 |     - sfra.models.Response
  7 | 
  8 | ## Description
  9 | 
 10 | The SFRA Response object is a local wrapper around the global response object that provides enhanced functionality for SFRA (Storefront Reference Architecture) applications. This class serves as a centralized interface for managing response data, rendering templates, handling redirects, and controlling HTTP response behavior. The Response object maintains state for template rendering, view data, redirect URLs, logging messages, and HTTP headers while providing a consistent API for different types of responses (ISML templates, JSON, XML, Page Designer pages). It includes built-in support for caching, content type management, and response status codes, making it the primary interface for controller response handling in SFRA applications.
 11 | 
 12 | ## Properties
 13 | 
 14 | ### view
 15 | 
 16 | **Type:** String
 17 | 
 18 | The template name/path to be rendered.
 19 | 
 20 | ### viewData
 21 | 
 22 | **Type:** Object
 23 | 
 24 | Data object containing all variables to be passed to the template during rendering.
 25 | 
 26 | ### redirectUrl
 27 | 
 28 | **Type:** String
 29 | 
 30 | URL to redirect to when a redirect response is triggered.
 31 | 
 32 | ### redirectStatus
 33 | 
 34 | **Type:** String
 35 | 
 36 | HTTP status code for redirect responses (e.g., "301", "302").
 37 | 
 38 | ### messageLog
 39 | 
 40 | **Type:** Array
 41 | 
 42 | Collection of log messages for debugging and error output.
 43 | 
 44 | ### base
 45 | 
 46 | **Type:** dw.system.Response
 47 | 
 48 | Reference to the original global response object.
 49 | 
 50 | ### cachePeriod
 51 | 
 52 | **Type:** Number
 53 | 
 54 | Cache expiration period value.
 55 | 
 56 | ### cachePeriodUnit
 57 | 
 58 | **Type:** String
 59 | 
 60 | Unit for cache period (typically hours).
 61 | 
 62 | ### personalized
 63 | 
 64 | **Type:** Boolean
 65 | 
 66 | Indicates whether the response contains personalized content.
 67 | 
 68 | ### renderings
 69 | 
 70 | **Type:** Array
 71 | 
 72 | Collection of rendering steps to be executed in order.
 73 | 
 74 | ### isJson
 75 | 
 76 | **Type:** Boolean
 77 | 
 78 | Flag indicating if the response should be rendered as JSON.
 79 | 
 80 | ### isXml
 81 | 
 82 | **Type:** Boolean
 83 | 
 84 | Flag indicating if the response should be rendered as XML.
 85 | 
 86 | ## Constructor Summary
 87 | 
 88 | ### Response
 89 | 
 90 | **Signature:** `Response(response)`
 91 | 
 92 | Creates a new SFRA Response object from the global response object.
 93 | 
 94 | **Parameters:**
 95 | - `response` (Object) - Global response object
 96 | 
 97 | ## Method Summary
 98 | 
 99 | ### render
100 | 
101 | **Signature:** `render(name, data) : void`
102 | 
103 | Stores template name and data for ISML template rendering.
104 | 
105 | ### json
106 | 
107 | **Signature:** `json(data) : void`
108 | 
109 | Configures response to render data as JSON.
110 | 
111 | ### xml
112 | 
113 | **Signature:** `xml(xmlString) : void`
114 | 
115 | Configures response to render data as XML.
116 | 
117 | ### page
118 | 
119 | **Signature:** `page(page, data, aspectAttributes) : void`
120 | 
121 | Configures response to render a Page Designer page.
122 | 
123 | ### redirect
124 | 
125 | **Signature:** `redirect(url) : void`
126 | 
127 | Sets up URL redirection for the response.
128 | 
129 | ### setRedirectStatus
130 | 
131 | **Signature:** `setRedirectStatus(redirectStatus) : void`
132 | 
133 | Sets the HTTP status code for redirects.
134 | 
135 | ### getViewData
136 | 
137 | **Signature:** `getViewData() : Object`
138 | 
139 | Retrieves the current view data object.
140 | 
141 | ### setViewData
142 | 
143 | **Signature:** `setViewData(data) : void`
144 | 
145 | Updates the view data with new data.
146 | 
147 | ### log
148 | 
149 | **Signature:** `log(...arguments) : void`
150 | 
151 | Logs messages for debugging and error output.
152 | 
153 | ### setContentType
154 | 
155 | **Signature:** `setContentType(type) : void`
156 | 
157 | Sets the HTTP content type for the response.
158 | 
159 | ### setStatusCode
160 | 
161 | **Signature:** `setStatusCode(code) : void`
162 | 
163 | Sets the HTTP status code for the response.
164 | 
165 | ### print
166 | 
167 | **Signature:** `print(message) : void`
168 | 
169 | Adds a print step to the rendering pipeline.
170 | 
171 | ### cacheExpiration
172 | 
173 | **Signature:** `cacheExpiration(period) : void`
174 | 
175 | Sets cache expiration period in hours.
176 | 
177 | ### setHttpHeader
178 | 
179 | **Signature:** `setHttpHeader(name, value) : void`
180 | 
181 | Adds a custom HTTP header to the response.
182 | 
183 | ## Method Detail
184 | 
185 | ### render
186 | 
187 | **Signature:** `render(name, data) : void`
188 | 
189 | **Description:** Stores template name and data for rendering an ISML template at execution time. The data is merged with existing view data, and a render step is added to the renderings pipeline.
190 | 
191 | **Parameters:**
192 | - `name` (String) - Path to the ISML template file
193 | - `data` (Object) - Data object to be passed to the template
194 | 
195 | **Returns:**
196 | void
197 | 
198 | ### json
199 | 
200 | **Signature:** `json(data) : void`
201 | 
202 | **Description:** Configures the response to render the provided data as JSON. Sets the isJson flag and merges data with existing view data.
203 | 
204 | **Parameters:**
205 | - `data` (Object) - Data object to be serialized as JSON
206 | 
207 | **Returns:**
208 | void
209 | 
210 | ### xml
211 | 
212 | **Signature:** `xml(xmlString) : void`
213 | 
214 | **Description:** Configures the response to render the provided XML string. Sets the isXml flag and stores the XML content in view data.
215 | 
216 | **Parameters:**
217 | - `xmlString` (String) - Valid XML string to be rendered
218 | 
219 | **Returns:**
220 | void
221 | 
222 | ### page
223 | 
224 | **Signature:** `page(page, data, aspectAttributes) : void`
225 | 
226 | **Description:** Configures the response to render a Page Designer page with optional aspect attributes for advanced page management.
227 | 
228 | **Parameters:**
229 | - `page` (String) - ID of the Page Designer page to render
230 | - `data` (Object) - Data object to be passed to the page
231 | - `aspectAttributes` (dw.util.HashMap) - Optional aspect attributes for PageMgr
232 | 
233 | **Returns:**
234 | void
235 | 
236 | ### redirect
237 | 
238 | **Signature:** `redirect(url) : void`
239 | 
240 | **Description:** Sets up URL redirection for the response. The redirect will be executed during response processing.
241 | 
242 | **Parameters:**
243 | - `url` (String) - Target URL for redirection
244 | 
245 | **Returns:**
246 | void
247 | 
248 | ### setRedirectStatus
249 | 
250 | **Signature:** `setRedirectStatus(redirectStatus) : void`
251 | 
252 | **Description:** Sets the HTTP status code for redirect responses. Common values are "301" for permanent redirects and "302" for temporary redirects.
253 | 
254 | **Parameters:**
255 | - `redirectStatus` (String) - HTTP status code for the redirect
256 | 
257 | **Returns:**
258 | void
259 | 
260 | ### getViewData
261 | 
262 | **Signature:** `getViewData() : Object`
263 | 
264 | **Description:** Retrieves the current view data object containing all variables that will be passed to the template during rendering.
265 | 
266 | **Returns:**
267 | Object containing all view data variables.
268 | 
269 | ### setViewData
270 | 
271 | **Signature:** `setViewData(data) : void`
272 | 
273 | **Description:** Updates the view data by merging the provided data object with existing view data. Existing properties with the same keys will be overwritten.
274 | 
275 | **Parameters:**
276 | - `data` (Object) - Data object to merge with existing view data
277 | 
278 | **Returns:**
279 | void
280 | 
281 | ### log
282 | 
283 | **Signature:** `log(...arguments) : void`
284 | 
285 | **Description:** Logs multiple arguments for debugging and error output. Objects and arrays are automatically JSON.stringified, while other types are converted to strings.
286 | 
287 | **Parameters:**
288 | - `...arguments` - Variable number of arguments to log
289 | 
290 | **Returns:**
291 | void
292 | 
293 | ### setContentType
294 | 
295 | **Signature:** `setContentType(type) : void`
296 | 
297 | **Description:** Sets the HTTP content type header for the response (e.g., "application/json", "text/xml", "text/html").
298 | 
299 | **Parameters:**
300 | - `type` (String) - MIME type for the response content
301 | 
302 | **Returns:**
303 | void
304 | 
305 | ### setStatusCode
306 | 
307 | **Signature:** `setStatusCode(code) : void`
308 | 
309 | **Description:** Sets the HTTP status code for the response (e.g., 200, 404, 500).
310 | 
311 | **Parameters:**
312 | - `code` (Number) - Valid HTTP status code
313 | 
314 | **Returns:**
315 | void
316 | 
317 | ### print
318 | 
319 | **Signature:** `print(message) : void`
320 | 
321 | **Description:** Adds a print step to the rendering pipeline that will output the message directly to the response stream.
322 | 
323 | **Parameters:**
324 | - `message` (String) - Message to be printed to the response
325 | 
326 | **Returns:**
327 | void
328 | 
329 | ### cacheExpiration
330 | 
331 | **Signature:** `cacheExpiration(period) : void`
332 | 
333 | **Description:** Sets the cache expiration period for the current page response in hours from the current time.
334 | 
335 | **Parameters:**
336 | - `period` (Number) - Number of hours from current time for cache expiration
337 | 
338 | **Returns:**
339 | void
340 | 
341 | ### setHttpHeader
342 | 
343 | **Signature:** `setHttpHeader(name, value) : void`
344 | 
345 | **Description:** Adds a custom HTTP header to the response with the specified name and value.
346 | 
347 | **Parameters:**
348 | - `name` (String) - Header name
349 | - `value` (String) - Header value
350 | 
351 | **Returns:**
352 | void
353 | 
354 | ## Property Details
355 | 
356 | ### viewData
357 | 
358 | **Type:** Object
359 | 
360 | **Description:** Central data object that accumulates all variables to be passed to templates during rendering. Data is merged using the assign utility, allowing for incremental data building throughout controller execution.
361 | 
362 | ### renderings
363 | 
364 | **Type:** Array
365 | 
366 | **Description:** Ordered collection of rendering steps that define how the response should be processed. Each step contains:
367 | 
368 | **For Render Steps:**
369 | - `type` - Always "render"
370 | - `subType` - Type of rendering ("isml", "json", "xml", "page")
371 | - `view` - Template name (for ISML)
372 | - `page` - Page ID (for Page Designer)
373 | - `aspectAttributes` - Page attributes (for Page Designer)
374 | 
375 | **For Print Steps:**
376 | - `type` - Always "print"
377 | - `message` - Message to output
378 | 
379 | ### messageLog
380 | 
381 | **Type:** Array
382 | 
383 | **Description:** Collection of debug and error messages logged during controller execution. Messages are automatically formatted, with objects and arrays converted to JSON strings.
384 | 
385 | ### base
386 | 
387 | **Type:** dw.system.Response
388 | 
389 | **Description:** Reference to the original global response object, providing access to core response functionality like setting HTTP headers, content types, and status codes.
390 | 
391 | ---
392 | 
```
Page 17/61FirstPrevNextLast