@elevasis/core 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/README.md +41 -41
- package/src/__tests__/publish.test.ts +18 -18
- package/src/__tests__/{template-foundations-compatibility.test.ts → template-core-compatibility.test.ts} +99 -99
- package/src/_gen/__tests__/__snapshots__/contracts.md.snap +1135 -1131
- package/src/_gen/__tests__/scaffold-contracts.test.ts +53 -53
- package/src/_gen/scaffold-contracts.ts +45 -45
- package/src/auth/multi-tenancy/credentials/README.md +38 -38
- package/src/auth/multi-tenancy/credentials/index.ts +6 -6
- package/src/auth/multi-tenancy/credentials/server/encryption.ts +39 -39
- package/src/auth/multi-tenancy/credentials/server/service.ts +60 -60
- package/src/auth/multi-tenancy/index.ts +17 -17
- package/src/auth/multi-tenancy/invitations/api-schemas.ts +107 -107
- package/src/auth/multi-tenancy/invitations/index.ts +37 -37
- package/src/auth/multi-tenancy/invitations/invitation.ts +86 -86
- package/src/auth/multi-tenancy/invitations/server/index.ts +25 -25
- package/src/auth/multi-tenancy/invitations/server/transforms.ts +24 -24
- package/src/auth/multi-tenancy/invitations/server/workos.ts +24 -24
- package/src/auth/multi-tenancy/invitations/supabase.ts +50 -50
- package/src/auth/multi-tenancy/memberships/api-schemas.ts +126 -126
- package/src/auth/multi-tenancy/memberships/index.ts +21 -21
- package/src/auth/multi-tenancy/memberships/membership.ts +138 -138
- package/src/auth/multi-tenancy/memberships/server/index.ts +15 -15
- package/src/auth/multi-tenancy/memberships/server/transforms.ts +32 -32
- package/src/auth/multi-tenancy/memberships/server/workos.ts +21 -21
- package/src/auth/multi-tenancy/memberships/supabase.ts +46 -46
- package/src/auth/multi-tenancy/organizations/api-schemas.ts +128 -128
- package/src/auth/multi-tenancy/organizations/index.ts +23 -23
- package/src/auth/multi-tenancy/organizations/organization.ts +24 -24
- package/src/auth/multi-tenancy/organizations/server/index.ts +10 -10
- package/src/auth/multi-tenancy/organizations/server/transforms.ts +35 -35
- package/src/auth/multi-tenancy/organizations/server/workos.ts +20 -20
- package/src/auth/multi-tenancy/types.ts +83 -83
- package/src/auth/multi-tenancy/users/api-schemas.ts +194 -194
- package/src/auth/multi-tenancy/users/index.ts +27 -27
- package/src/auth/multi-tenancy/users/server/index.ts +19 -19
- package/src/auth/multi-tenancy/users/server/transforms.ts +21 -21
- package/src/auth/multi-tenancy/users/server/workos.ts +16 -16
- package/src/auth/multi-tenancy/users/user.ts +65 -65
- package/src/business/README.md +52 -52
- package/src/business/__tests__/entities-published.test.ts +33 -33
- package/src/business/acquisition/api-schemas.ts +759 -759
- package/src/business/acquisition/index.ts +109 -109
- package/src/business/acquisition/types.ts +402 -402
- package/src/business/base-entities.test.ts +481 -481
- package/src/business/base-entities.ts +241 -241
- package/src/business/entities-published.ts +24 -24
- package/src/business/index.ts +15 -15
- package/src/business/pdf/browser/pdfmake-browser.ts +229 -229
- package/src/business/pdf/index.ts +10 -10
- package/src/business/pdf/server/index.ts +21 -21
- package/src/business/pdf/server/themes/default.ts +8 -8
- package/src/business/pdf/server/themes/index.ts +9 -9
- package/src/business/pdf/server/themes/types.ts +8 -8
- package/src/business/pdf/types.ts +272 -272
- package/src/business/projects/index.ts +2 -1
- package/src/business/projects/sse-events.ts +21 -0
- package/src/business/projects/types.ts +89 -89
- package/src/business/sales/api-schemas.ts +75 -75
- package/src/business/seo/__tests__/linking.test.ts +549 -549
- package/src/business/seo/__tests__/types.test.ts +404 -404
- package/src/business/seo/index.ts +2 -2
- package/src/business/seo/linking.ts +281 -281
- package/src/business/seo/types.ts +199 -199
- package/src/commands/queue/index.ts +3 -3
- package/src/commands/queue/schemas.test.ts +593 -593
- package/src/commands/queue/schemas.ts +125 -125
- package/src/commands/queue/sse-events.ts +61 -61
- package/src/commands/queue/types/action.ts +52 -52
- package/src/commands/queue/types/checkpoint.ts +44 -44
- package/src/commands/queue/types/index.ts +7 -7
- package/src/commands/queue/types/task.ts +116 -116
- package/src/commands/queue/types.ts +14 -14
- package/src/content/distribution-metadata.ts +61 -61
- package/src/content/index.ts +10 -10
- package/src/deployments/index.ts +22 -22
- package/src/execution/core/__tests__/archived-logs.test.ts +72 -72
- package/src/execution/core/index.ts +11 -11
- package/src/execution/core/runner-types.ts +80 -80
- package/src/execution/core/server/environment.ts +31 -31
- package/src/execution/core/sse-executions.ts +119 -119
- package/src/execution/core/types.ts +29 -29
- package/src/execution/engine/__tests__/fixtures/test-agents.ts +4 -4
- package/src/execution/engine/__tests__/timeout.test.ts +565 -565
- package/src/execution/engine/agent/__tests__/errors.test.ts +508 -508
- package/src/execution/engine/agent/actions/__tests__/processor.test.ts +531 -531
- package/src/execution/engine/agent/actions/executor.ts +205 -205
- package/src/execution/engine/agent/actions/navigate-knowledge-executor.ts +230 -230
- package/src/execution/engine/agent/actions/processor.ts +116 -116
- package/src/execution/engine/agent/actions/types.ts +70 -70
- package/src/execution/engine/agent/core/agent.ts +810 -810
- package/src/execution/engine/agent/core/types.ts +155 -155
- package/src/execution/engine/agent/errors.ts +251 -251
- package/src/execution/engine/agent/index.ts +78 -78
- package/src/execution/engine/agent/knowledge-map/types.ts +106 -106
- package/src/execution/engine/agent/knowledge-map/utils.ts +101 -101
- package/src/execution/engine/agent/memory/__tests__/manager.test.ts +754 -754
- package/src/execution/engine/agent/memory/domains.ts +99 -99
- package/src/execution/engine/agent/memory/manager.ts +365 -365
- package/src/execution/engine/agent/memory/processor.ts +66 -66
- package/src/execution/engine/agent/memory/types.ts +90 -90
- package/src/execution/engine/agent/memory/utils.ts +134 -134
- package/src/execution/engine/agent/observability/logging.ts +467 -467
- package/src/execution/engine/agent/observability/types.ts +64 -64
- package/src/execution/engine/agent/reasoning/adapters/agent-adapter-helpers.ts +349 -349
- package/src/execution/engine/agent/reasoning/processor.ts +92 -92
- package/src/execution/engine/agent/reasoning/prompt-sections/base-actions.ts +134 -134
- package/src/execution/engine/agent/reasoning/prompt-sections/completion.ts +49 -49
- package/src/execution/engine/agent/reasoning/prompt-sections/knowledge-map.ts +93 -93
- package/src/execution/engine/agent/reasoning/prompt-sections/memory.ts +65 -65
- package/src/execution/engine/agent/reasoning/prompt-sections/tools.ts +44 -44
- package/src/execution/engine/agent/reasoning/request-builder.ts +169 -169
- package/src/execution/engine/agent/reasoning/types.ts +18 -18
- package/src/execution/engine/base/errors.ts +118 -118
- package/src/execution/engine/base/index.ts +2 -2
- package/src/execution/engine/base/logging.ts +31 -31
- package/src/execution/engine/base/serialization.ts +324 -324
- package/src/execution/engine/base/types.ts +126 -126
- package/src/execution/engine/base/utils.ts +41 -41
- package/src/execution/engine/index.ts +434 -434
- package/src/execution/engine/interface/index.ts +1 -1
- package/src/execution/engine/interface/types.ts +62 -62
- package/src/execution/engine/llm/__tests__/model-info.test.ts +50 -50
- package/src/execution/engine/llm/__tests__/model-validation.test.ts +321 -321
- package/src/execution/engine/llm/__tests__/response-schema-validator.test.ts +115 -115
- package/src/execution/engine/llm/adapters/__tests__/adapter-factory.test.ts +375 -375
- package/src/execution/engine/llm/adapters/__tests__/anthropic-adapter.test.ts +463 -463
- package/src/execution/engine/llm/adapters/__tests__/anthropic.integration.test.ts +177 -177
- package/src/execution/engine/llm/adapters/__tests__/google-adapter.test.ts +722 -722
- package/src/execution/engine/llm/adapters/__tests__/google.integration.test.ts +376 -376
- package/src/execution/engine/llm/adapters/__tests__/openai-adapter.test.ts +551 -551
- package/src/execution/engine/llm/adapters/__tests__/openrouter-adapter.test.ts +563 -563
- package/src/execution/engine/llm/adapters/__tests__/openrouter.integration.test.ts +105 -105
- package/src/execution/engine/llm/adapters/__tests__/universal-adapter.test.ts +537 -537
- package/src/execution/engine/llm/adapters/circuit-breaker.ts +147 -147
- package/src/execution/engine/llm/adapters/index.ts +17 -17
- package/src/execution/engine/llm/adapters/mock-adapter.ts +116 -116
- package/src/execution/engine/llm/adapters/server/adapter-factory.ts +130 -130
- package/src/execution/engine/llm/adapters/server/anthropic.ts +137 -137
- package/src/execution/engine/llm/adapters/server/google.ts +283 -283
- package/src/execution/engine/llm/adapters/server/index.ts +12 -12
- package/src/execution/engine/llm/adapters/server/openai.ts +206 -206
- package/src/execution/engine/llm/adapters/server/openrouter.ts +235 -235
- package/src/execution/engine/llm/adapters/universal-adapter.ts +230 -230
- package/src/execution/engine/llm/errors.ts +186 -186
- package/src/execution/engine/llm/model-info.ts +332 -332
- package/src/execution/engine/llm/response-schema-validator.ts +113 -113
- package/src/execution/engine/llm/types.ts +86 -86
- package/src/execution/engine/test-utils/index.ts +6 -6
- package/src/execution/engine/test-utils/mocks.ts +56 -56
- package/src/execution/engine/tools/integration/base-integration-adapter.ts +50 -50
- package/src/execution/engine/tools/integration/index.ts +53 -53
- package/src/execution/engine/tools/integration/server/adapters/anymailfinder/anymailfinder-adapter.ts +73 -73
- package/src/execution/engine/tools/integration/server/adapters/anymailfinder/anymailfinder-tools.ts +209 -209
- package/src/execution/engine/tools/integration/server/adapters/anymailfinder/fetch/find-company-email/index.ts +82 -82
- package/src/execution/engine/tools/integration/server/adapters/anymailfinder/fetch/find-decision-maker-email/index.ts +122 -122
- package/src/execution/engine/tools/integration/server/adapters/anymailfinder/fetch/find-person-email/index.ts +89 -89
- package/src/execution/engine/tools/integration/server/adapters/anymailfinder/fetch/verify-email/index.ts +84 -84
- package/src/execution/engine/tools/integration/server/adapters/anymailfinder/index.ts +16 -16
- package/src/execution/engine/tools/integration/server/adapters/apify/__tests__/apify-run-actor.integration.test.ts +293 -293
- package/src/execution/engine/tools/integration/server/adapters/apify/apify-adapter.ts +100 -100
- package/src/execution/engine/tools/integration/server/adapters/apify/apify-tools.ts +217 -217
- package/src/execution/engine/tools/integration/server/adapters/apify/fetch/get-dataset-items/index.ts +92 -92
- package/src/execution/engine/tools/integration/server/adapters/apify/fetch/run-actor/index.ts +218 -218
- package/src/execution/engine/tools/integration/server/adapters/apify/fetch/start-actor/index.ts +87 -87
- package/src/execution/engine/tools/integration/server/adapters/apify/index.ts +11 -11
- package/src/execution/engine/tools/integration/server/adapters/attio/__tests__/attio-crud.integration.test.ts +361 -361
- package/src/execution/engine/tools/integration/server/adapters/attio/attio-adapter.ts +162 -162
- package/src/execution/engine/tools/integration/server/adapters/attio/attio-tools.ts +594 -594
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/create-attribute/index.ts +214 -214
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/create-note/index.ts +152 -152
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/create-record/index.ts +141 -141
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/delete-note/index.ts +86 -86
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/delete-record/index.ts +105 -105
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/get-record/index.ts +118 -118
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-attributes/index.ts +165 -165
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-notes/index.ts +96 -96
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-objects/index.ts +104 -104
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-records/index.ts +156 -156
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/update-attribute/index.ts +220 -220
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/update-record/index.ts +140 -140
- package/src/execution/engine/tools/integration/server/adapters/attio/fetch/utils/types.ts +146 -146
- package/src/execution/engine/tools/integration/server/adapters/attio/index.ts +31 -31
- package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-adapter.ts +210 -210
- package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-tools.ts +104 -104
- package/src/execution/engine/tools/integration/server/adapters/google-sheets/__tests__/google-sheets.integration.test.ts +261 -261
- package/src/execution/engine/tools/integration/server/adapters/google-sheets/google-sheets-adapter.ts +1189 -1189
- package/src/execution/engine/tools/integration/server/adapters/google-sheets/google-sheets-tools.ts +641 -641
- package/src/execution/engine/tools/integration/server/adapters/google-sheets/index.ts +18 -18
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/activate-campaign/index.ts +86 -86
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/add-to-campaign/__tests__/index.test.ts +289 -289
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/add-to-campaign/index.ts +154 -154
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/bulk-add-leads/__tests__/index.test.ts +325 -325
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/bulk-add-leads/index.ts +153 -153
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/bulk-delete-leads/index.ts +84 -84
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/create-campaign/index.ts +125 -125
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/create-inbox-test/index.ts +107 -107
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/delete-campaign/index.ts +85 -85
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-account-health/index.ts +91 -91
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-campaign/index.ts +92 -92
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-campaign-analytics/__tests__/index.test.ts +195 -195
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-campaign-analytics/index.ts +113 -113
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-daily-campaign-analytics/index.ts +104 -104
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-emails/index.ts +155 -155
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-step-analytics/__tests__/index.test.ts +196 -196
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/get-step-analytics/index.ts +102 -102
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/list-campaigns/__tests__/index.test.ts +189 -189
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/list-campaigns/index.ts +87 -87
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/list-leads/index.ts +112 -112
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/patch-lead/index.ts +76 -76
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/pause-campaign/index.ts +86 -86
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/remove-from-subsequence/index.ts +98 -98
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/send-reply/index.ts +126 -126
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/update-campaign/__tests__/index.test.ts +193 -193
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/update-campaign/index.ts +99 -99
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/update-interest-status/__tests__/index.test.ts +621 -621
- package/src/execution/engine/tools/integration/server/adapters/instantly/fetch/update-interest-status/index.ts +125 -125
- package/src/execution/engine/tools/integration/server/adapters/instantly/index.ts +29 -29
- package/src/execution/engine/tools/integration/server/adapters/instantly/instantly-adapter.ts +178 -178
- package/src/execution/engine/tools/integration/server/adapters/instantly/instantly-tools.ts +1473 -1473
- package/src/execution/engine/tools/integration/server/adapters/millionverifier/fetch/check-credits/index.ts +59 -59
- package/src/execution/engine/tools/integration/server/adapters/millionverifier/fetch/verify-email/index.ts +102 -102
- package/src/execution/engine/tools/integration/server/adapters/millionverifier/index.ts +17 -17
- package/src/execution/engine/tools/integration/server/adapters/millionverifier/millionverifier-adapter.ts +80 -80
- package/src/execution/engine/tools/integration/server/adapters/millionverifier/millionverifier-tools.ts +102 -102
- package/src/execution/engine/tools/integration/server/adapters/resend/fetch/get-email/index.ts +102 -102
- package/src/execution/engine/tools/integration/server/adapters/resend/fetch/send-email/index.ts +134 -134
- package/src/execution/engine/tools/integration/server/adapters/resend/fetch/utils/types.ts +75 -75
- package/src/execution/engine/tools/integration/server/adapters/resend/index.ts +27 -27
- package/src/execution/engine/tools/integration/server/adapters/resend/resend-adapter.ts +108 -108
- package/src/execution/engine/tools/integration/server/adapters/resend/resend-tools.ts +132 -132
- package/src/execution/engine/tools/integration/server/adapters/signature-api/fetch/create-envelope/index.ts +274 -274
- package/src/execution/engine/tools/integration/server/adapters/signature-api/fetch/download-document/index.ts +230 -230
- package/src/execution/engine/tools/integration/server/adapters/signature-api/fetch/get-envelope/index.ts +133 -133
- package/src/execution/engine/tools/integration/server/adapters/signature-api/fetch/void-envelope/index.ts +90 -90
- package/src/execution/engine/tools/integration/server/adapters/stripe/fetch/utils/types.ts +210 -210
- package/src/execution/engine/tools/integration/server/adapters/stripe/stripe-adapter.ts +517 -517
- package/src/execution/engine/tools/integration/server/adapters/stripe/stripe-tools.ts +309 -309
- package/src/execution/engine/tools/integration/server/adapters/tomba/fetch/domain-search/index.ts +133 -133
- package/src/execution/engine/tools/integration/server/adapters/tomba/fetch/email-finder/index.ts +122 -122
- package/src/execution/engine/tools/integration/server/adapters/tomba/fetch/email-verifier/index.ts +111 -111
- package/src/execution/engine/tools/integration/server/adapters/tomba/index.ts +11 -11
- package/src/execution/engine/tools/integration/server/adapters/tomba/tomba-adapter.ts +78 -78
- package/src/execution/engine/tools/integration/server/adapters/tomba/tomba-tools.ts +222 -222
- package/src/execution/engine/tools/integration/server/index.ts +61 -61
- package/src/execution/engine/tools/integration/service.ts +161 -161
- package/src/execution/engine/tools/integration/tool.ts +253 -253
- package/src/execution/engine/tools/integration/types/anymailfinder.ts +74 -74
- package/src/execution/engine/tools/integration/types/apify.ts +92 -92
- package/src/execution/engine/tools/integration/types/index.ts +19 -19
- package/src/execution/engine/tools/integration/types/instantly.ts +557 -557
- package/src/execution/engine/tools/integration/types/millionverifier.ts +56 -56
- package/src/execution/engine/tools/integration/types/stripe.ts +162 -162
- package/src/execution/engine/tools/integration/types/tomba.ts +94 -94
- package/src/execution/engine/tools/lead-service-types.ts +884 -884
- package/src/execution/engine/tools/llm/index.ts +11 -11
- package/src/execution/engine/tools/llm/server/index.ts +8 -8
- package/src/execution/engine/tools/llm/server/llm-call-tool.ts +118 -118
- package/src/execution/engine/tools/platform/__tests__/pdf.test.ts +441 -441
- package/src/execution/engine/tools/platform/acquisition/company-tools.ts +248 -248
- package/src/execution/engine/tools/platform/acquisition/contact-tools.ts +319 -319
- package/src/execution/engine/tools/platform/acquisition/index.ts +43 -43
- package/src/execution/engine/tools/platform/acquisition/list-tools.ts +148 -148
- package/src/execution/engine/tools/platform/acquisition/types.ts +260 -260
- package/src/execution/engine/tools/platform/email/index.ts +122 -122
- package/src/execution/engine/tools/platform/email/types.ts +96 -96
- package/src/execution/engine/tools/platform/index.ts +157 -157
- package/src/execution/engine/tools/platform/notification.ts +81 -81
- package/src/execution/engine/tools/platform/pdf/index.ts +110 -110
- package/src/execution/engine/tools/platform/pdf/types.ts +77 -77
- package/src/execution/engine/tools/platform/scheduler.ts +87 -87
- package/src/execution/engine/tools/platform/storage/index.ts +370 -370
- package/src/execution/engine/tools/platform/types.ts +148 -148
- package/src/execution/engine/tools/registry.ts +700 -699
- package/src/execution/engine/tools/tool-maps.ts +786 -786
- package/src/execution/engine/tools/types.ts +233 -233
- package/src/execution/engine/workflow/__tests__/errors.test.ts +139 -139
- package/src/execution/engine/workflow/errors.ts +63 -63
- package/src/execution/engine/workflow/helpers/index.ts +11 -11
- package/src/execution/engine/workflow/helpers/server/index.ts +8 -8
- package/src/execution/engine/workflow/helpers/server/llm-call.ts +93 -93
- package/src/execution/engine/workflow/index.ts +19 -19
- package/src/execution/engine/workflow/log-truncate.ts +26 -26
- package/src/execution/engine/workflow/logging.ts +191 -191
- package/src/execution/engine/workflow/types.ts +182 -182
- package/src/execution/engine/workflow/utils.ts +280 -280
- package/src/execution/engine/workflow/workflow.ts +168 -168
- package/src/execution/index.ts +3 -3
- package/src/execution/scheduler/__tests__/api-schemas.test.ts +733 -733
- package/src/execution/scheduler/__tests__/utils.test.ts +1009 -1009
- package/src/execution/scheduler/api-schemas.ts +296 -296
- package/src/execution/scheduler/index.ts +50 -50
- package/src/execution/scheduler/schemas.ts +264 -264
- package/src/execution/scheduler/types.ts +111 -111
- package/src/execution/scheduler/utils.ts +364 -364
- package/src/forms/index.ts +7 -7
- package/src/forms/schemas.ts +69 -69
- package/src/forms/types.ts +70 -70
- package/src/index.ts +71 -60
- package/src/integrations/credentials/__tests__/schemas.test.ts +82 -82
- package/src/integrations/credentials/__tests__/utils.test.ts +144 -144
- package/src/integrations/credentials/api-schemas.ts +143 -143
- package/src/integrations/credentials/index.ts +32 -32
- package/src/integrations/credentials/schemas.ts +164 -164
- package/src/integrations/credentials/utils.ts +59 -59
- package/src/integrations/oauth/__tests__/provider-registry.test.ts +59 -59
- package/src/integrations/oauth/api-schemas.ts +92 -92
- package/src/integrations/oauth/index.ts +19 -19
- package/src/integrations/oauth/provider-registry.ts +61 -61
- package/src/integrations/oauth/server/__tests__/refresh-concurrent.test.ts +183 -183
- package/src/integrations/oauth/server/__tests__/refresh.test.ts +577 -577
- package/src/integrations/oauth/server/credentials.ts +39 -39
- package/src/integrations/oauth/server/refresh.ts +214 -214
- package/src/integrations/oauth/types.ts +34 -34
- package/src/integrations/webhook-endpoints/__tests__/api-schemas.test.ts +318 -318
- package/src/integrations/webhook-endpoints/api-schemas.ts +102 -102
- package/src/integrations/webhook-endpoints/index.ts +28 -28
- package/src/integrations/webhook-endpoints/types.ts +51 -51
- package/src/operations/activities/api-schemas.ts +79 -79
- package/src/operations/activities/index.ts +9 -9
- package/src/operations/activities/sse-events.ts +30 -30
- package/src/operations/activities/types.ts +63 -63
- package/src/operations/debug-logs/client.ts +60 -60
- package/src/operations/debug-logs/debug-logger.ts +83 -83
- package/src/operations/debug-logs/index.ts +8 -8
- package/src/operations/debug-logs/server.ts +19 -19
- package/src/operations/debug-logs/types.ts +33 -33
- package/src/operations/index.ts +50 -50
- package/src/operations/notifications/api-schemas.ts +91 -91
- package/src/operations/notifications/index.ts +3 -3
- package/src/operations/notifications/sse-events.ts +21 -21
- package/src/operations/notifications/types.ts +47 -47
- package/src/operations/observability/__tests__/openrouter-cost-flow.test.ts +297 -297
- package/src/operations/observability/__tests__/utils.test.ts +54 -54
- package/src/operations/observability/ai-usage-collector.ts +64 -64
- package/src/operations/observability/index.ts +13 -13
- package/src/operations/observability/metrics-collector.ts +49 -49
- package/src/operations/observability/schemas.ts +39 -39
- package/src/operations/observability/types.ts +463 -463
- package/src/operations/observability/utils.ts +77 -77
- package/src/operations/sessions/__tests__/manager.test.ts +821 -821
- package/src/operations/sessions/index.ts +26 -26
- package/src/operations/sessions/server/manager.ts +90 -90
- package/src/operations/sessions/server/session.ts +180 -180
- package/src/operations/sessions/types.ts +98 -98
- package/src/operations/triggers/index.ts +12 -12
- package/src/operations/triggers/webhook/definitions/instantly-account-error.ts +44 -44
- package/src/operations/triggers/webhook/definitions/instantly-auto-reply-received.ts +51 -51
- package/src/operations/triggers/webhook/definitions/instantly-campaign-completed.ts +45 -45
- package/src/operations/triggers/webhook/definitions/instantly-email-bounced.ts +49 -49
- package/src/operations/triggers/webhook/definitions/instantly-lead-unsubscribed.ts +45 -45
- package/src/operations/triggers/webhook/definitions/instantly-reply-received.ts +54 -54
- package/src/operations/triggers/webhook/index.ts +35 -35
- package/src/operations/triggers/webhook/types.ts +74 -74
- package/src/organization-model/README.md +97 -97
- package/src/organization-model/__tests__/defaults.test.ts +175 -175
- package/src/organization-model/__tests__/domains/customers.test.ts +295 -295
- package/src/organization-model/__tests__/domains/goals.test.ts +479 -479
- package/src/organization-model/__tests__/domains/identity.test.ts +279 -279
- package/src/organization-model/__tests__/domains/navigation.test.ts +212 -212
- package/src/organization-model/__tests__/domains/offerings.test.ts +419 -419
- package/src/organization-model/__tests__/domains/operations.test.ts +203 -203
- package/src/organization-model/__tests__/domains/resource-mappings.test.ts +362 -362
- package/src/organization-model/__tests__/domains/roles.test.ts +347 -347
- package/src/organization-model/__tests__/domains/statuses.test.ts +243 -243
- package/src/organization-model/__tests__/foundation.test.ts +105 -105
- package/src/organization-model/__tests__/graph.test.ts +894 -894
- package/src/organization-model/__tests__/resolve.test.ts +690 -690
- package/src/organization-model/__tests__/schema.test.ts +407 -407
- package/src/organization-model/contracts.ts +14 -14
- package/src/organization-model/defaults.ts +148 -148
- package/src/organization-model/domains/branding.ts +22 -22
- package/src/organization-model/domains/customers.ts +75 -75
- package/src/organization-model/domains/features.ts +22 -22
- package/src/organization-model/domains/goals.ts +80 -80
- package/src/organization-model/domains/identity.ts +94 -94
- package/src/organization-model/domains/navigation.ts +391 -391
- package/src/organization-model/domains/offerings.ts +66 -66
- package/src/organization-model/domains/operations.ts +85 -85
- package/src/organization-model/domains/projects.ts +48 -48
- package/src/organization-model/domains/prospecting.ts +33 -33
- package/src/organization-model/domains/roles.ts +55 -55
- package/src/organization-model/domains/sales.ts +94 -94
- package/src/organization-model/domains/shared.ts +62 -62
- package/src/organization-model/domains/statuses.ts +130 -130
- package/src/organization-model/foundation.ts +97 -97
- package/src/organization-model/graph/build.ts +399 -399
- package/src/organization-model/graph/index.ts +4 -4
- package/src/organization-model/graph/schema.ts +48 -48
- package/src/organization-model/graph/types.ts +40 -40
- package/src/organization-model/index.ts +13 -13
- package/src/organization-model/organization-graph.mdx +272 -272
- package/src/organization-model/organization-model.mdx +320 -320
- package/src/organization-model/published.ts +85 -85
- package/src/organization-model/resolve.ts +66 -66
- package/src/organization-model/schema.ts +287 -287
- package/src/organization-model/types.ts +46 -46
- package/src/platform/api/index.ts +1 -1
- package/src/platform/api/types.ts +35 -35
- package/src/platform/constants/http.ts +37 -37
- package/src/platform/constants/index.ts +5 -5
- package/src/platform/constants/limits.ts +32 -32
- package/src/platform/constants/resilience.ts +51 -51
- package/src/platform/constants/timeouts.ts +20 -20
- package/src/platform/constants/versions.ts +3 -3
- package/src/platform/registry/__tests__/resource-registry-static.test.ts +347 -347
- package/src/platform/registry/__tests__/resource-registry.integration.test.ts +1028 -1028
- package/src/platform/registry/__tests__/resource-registry.list-executable.test.ts +393 -393
- package/src/platform/registry/__tests__/resource-registry.test.ts +2005 -2005
- package/src/platform/registry/__tests__/serialization.test.ts +1127 -1127
- package/src/platform/registry/command-view.ts +180 -180
- package/src/platform/registry/domains.ts +165 -165
- package/src/platform/registry/index.ts +93 -93
- package/src/platform/registry/reserved.ts +24 -24
- package/src/platform/registry/resource-metadata.ts +59 -59
- package/src/platform/registry/resource-registry.command-queue-groups.test.ts +129 -129
- package/src/platform/registry/resource-registry.ts +876 -876
- package/src/platform/registry/serialization.ts +273 -273
- package/src/platform/registry/serialized-types.ts +231 -231
- package/src/platform/registry/stats-types.ts +66 -66
- package/src/platform/registry/types.ts +404 -404
- package/src/platform/registry/validation.ts +513 -513
- package/src/platform/resilience/__tests__/rate-limiter.test.ts +471 -471
- package/src/platform/resilience/circuit-breaker.ts +164 -164
- package/src/platform/resilience/errors.ts +68 -68
- package/src/platform/resilience/http-error-mapper.ts +129 -129
- package/src/platform/resilience/index.ts +93 -93
- package/src/platform/resilience/rate-limiter-types.ts +46 -46
- package/src/platform/resilience/rate-limiter.ts +140 -140
- package/src/platform/resilience/retry.ts +89 -89
- package/src/platform/resilience/timeout.ts +63 -63
- package/src/platform/sse/events.ts +37 -34
- package/src/platform/sse/index.ts +7 -7
- package/src/platform/utils/__tests__/validation.test.ts +1083 -1083
- package/src/platform/utils/currency.ts +96 -96
- package/src/platform/utils/debounce.ts +52 -52
- package/src/platform/utils/error.ts +41 -41
- package/src/platform/utils/hmac.test.ts +97 -97
- package/src/platform/utils/index.ts +32 -32
- package/src/platform/utils/server/betterstack-logger.ts +210 -210
- package/src/platform/utils/server/hmac.ts +44 -44
- package/src/platform/utils/server/unsubscribe.ts +111 -111
- package/src/platform/utils/token-counter.ts +96 -96
- package/src/platform/utils/validation.ts +425 -425
- package/src/projects/api-schemas.ts +268 -268
- package/src/published.ts +1 -1
- package/src/reference/_generated/contracts.md +611 -607
- package/src/reference/glossary.md +105 -105
- package/src/requests/__tests__/api-schemas.test.ts +277 -277
- package/src/requests/api-schemas.ts +83 -83
- package/src/requests/index.ts +1 -1
- package/src/scaffold-registry/__tests__/index.test.ts +17 -0
- package/src/scaffold-registry/__tests__/schema.test.ts +329 -230
- package/src/scaffold-registry/index.ts +205 -189
- package/src/scaffold-registry/schema.ts +196 -128
- package/src/server.ts +272 -272
- package/src/supabase/database.types.ts +2719 -2719
- package/src/supabase/helpers.ts +20 -20
- package/src/supabase/index.ts +52 -52
- package/src/supabase/server/client.ts +58 -58
- package/src/test-utils/README.md +38 -38
- package/src/test-utils/browser-mocks.ts +54 -54
- package/src/test-utils/fixtures/api-keys.ts +52 -52
- package/src/test-utils/fixtures/index.ts +4 -4
- package/src/test-utils/fixtures/memberships.ts +80 -80
- package/src/test-utils/fixtures/organizations.ts +69 -69
- package/src/test-utils/fixtures/users.ts +79 -79
- package/src/test-utils/index.ts +11 -11
- package/src/test-utils/mocks/index.ts +2 -2
- package/src/test-utils/mocks/supabase.ts +142 -142
- package/src/test-utils/mocks/workos.ts +108 -108
- package/src/test-utils/rls/RLSTestContext.ts +556 -556
- package/src/test-utils/rls/index.ts +1 -1
|
@@ -1,549 +1,549 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { generateInternalLinks, generateAnchorText, getVerticalLabel } from '../linking'
|
|
3
|
-
import type { SeoPage } from '../types'
|
|
4
|
-
|
|
5
|
-
// =============================================================================
|
|
6
|
-
// Fixtures
|
|
7
|
-
// =============================================================================
|
|
8
|
-
|
|
9
|
-
const BASE_PAGE: SeoPage = {
|
|
10
|
-
id: '00000000-0000-0000-0000-000000000000',
|
|
11
|
-
organizationId: 'org-1',
|
|
12
|
-
pageType: 'pillar',
|
|
13
|
-
vertical: 'veterinary-clinics',
|
|
14
|
-
city: null,
|
|
15
|
-
state: null,
|
|
16
|
-
useCase: null,
|
|
17
|
-
slug: 'ai-automation/veterinary-clinics',
|
|
18
|
-
title: 'AI Automation for Veterinary Clinics',
|
|
19
|
-
metaDescription: null,
|
|
20
|
-
content: null,
|
|
21
|
-
faqItems: null,
|
|
22
|
-
schemaMarkup: null,
|
|
23
|
-
localData: null,
|
|
24
|
-
internalLinks: null,
|
|
25
|
-
status: 'published',
|
|
26
|
-
publishedAt: new Date('2026-01-01'),
|
|
27
|
-
refreshedAt: null,
|
|
28
|
-
createdAt: new Date('2026-01-01'),
|
|
29
|
-
updatedAt: new Date('2026-01-01')
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function makePage(overrides: Partial<SeoPage> & { id: string; slug: string }): SeoPage {
|
|
33
|
-
return { ...BASE_PAGE, ...overrides }
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Pillar pages for two verticals
|
|
37
|
-
const vetPillar = makePage({
|
|
38
|
-
id: 'vet-pillar',
|
|
39
|
-
pageType: 'pillar',
|
|
40
|
-
vertical: 'veterinary-clinics',
|
|
41
|
-
city: null,
|
|
42
|
-
slug: 'ai-automation/veterinary-clinics',
|
|
43
|
-
title: 'AI Automation for Veterinary Clinics'
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
const hvacPillar = makePage({
|
|
47
|
-
id: 'hvac-pillar',
|
|
48
|
-
pageType: 'pillar',
|
|
49
|
-
vertical: 'hvac-contractors',
|
|
50
|
-
city: null,
|
|
51
|
-
slug: 'ai-automation/hvac-contractors',
|
|
52
|
-
title: 'AI Automation for HVAC Contractors'
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
const autoPillar = makePage({
|
|
56
|
-
id: 'auto-pillar',
|
|
57
|
-
pageType: 'pillar',
|
|
58
|
-
vertical: 'auto-repair',
|
|
59
|
-
city: null,
|
|
60
|
-
slug: 'ai-automation/auto-repair',
|
|
61
|
-
title: 'AI Automation for Auto Repair Shops'
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
const pestPillar = makePage({
|
|
65
|
-
id: 'pest-pillar',
|
|
66
|
-
pageType: 'pillar',
|
|
67
|
-
vertical: 'pest-control',
|
|
68
|
-
city: null,
|
|
69
|
-
slug: 'ai-automation/pest-control',
|
|
70
|
-
title: 'AI Automation for Pest Control'
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
// Location pages
|
|
74
|
-
const vetIrvine = makePage({
|
|
75
|
-
id: 'vet-irvine',
|
|
76
|
-
pageType: 'location',
|
|
77
|
-
vertical: 'veterinary-clinics',
|
|
78
|
-
city: 'irvine',
|
|
79
|
-
state: 'CA',
|
|
80
|
-
slug: 'ai-automation/veterinary-clinics/irvine',
|
|
81
|
-
title: 'AI Automation for Vet Clinics in Irvine'
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
const vetAnaheim = makePage({
|
|
85
|
-
id: 'vet-anaheim',
|
|
86
|
-
pageType: 'location',
|
|
87
|
-
vertical: 'veterinary-clinics',
|
|
88
|
-
city: 'anaheim',
|
|
89
|
-
state: 'CA',
|
|
90
|
-
slug: 'ai-automation/veterinary-clinics/anaheim',
|
|
91
|
-
title: 'AI Automation for Vet Clinics in Anaheim'
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
const vetLosAngeles = makePage({
|
|
95
|
-
id: 'vet-la',
|
|
96
|
-
pageType: 'location',
|
|
97
|
-
vertical: 'veterinary-clinics',
|
|
98
|
-
city: 'los-angeles',
|
|
99
|
-
state: 'CA',
|
|
100
|
-
slug: 'ai-automation/veterinary-clinics/los-angeles',
|
|
101
|
-
title: 'AI Automation for Vet Clinics in Los Angeles'
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
const vetSantaAna = makePage({
|
|
105
|
-
id: 'vet-santa-ana',
|
|
106
|
-
pageType: 'location',
|
|
107
|
-
vertical: 'veterinary-clinics',
|
|
108
|
-
city: 'santa-ana',
|
|
109
|
-
state: 'CA',
|
|
110
|
-
slug: 'ai-automation/veterinary-clinics/santa-ana',
|
|
111
|
-
title: 'AI Automation for Vet Clinics in Santa Ana'
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
const vetSanDiego = makePage({
|
|
115
|
-
id: 'vet-san-diego',
|
|
116
|
-
pageType: 'location',
|
|
117
|
-
vertical: 'veterinary-clinics',
|
|
118
|
-
city: 'san-diego',
|
|
119
|
-
state: 'CA',
|
|
120
|
-
slug: 'ai-automation/veterinary-clinics/san-diego',
|
|
121
|
-
title: 'AI Automation for Vet Clinics in San Diego'
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
const vetSanFrancisco = makePage({
|
|
125
|
-
id: 'vet-sf',
|
|
126
|
-
pageType: 'location',
|
|
127
|
-
vertical: 'veterinary-clinics',
|
|
128
|
-
city: 'san-francisco',
|
|
129
|
-
state: 'CA',
|
|
130
|
-
slug: 'ai-automation/veterinary-clinics/san-francisco',
|
|
131
|
-
title: 'AI Automation for Vet Clinics in San Francisco'
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
// Cross-vertical location page in same city as vetIrvine
|
|
135
|
-
const hvacIrvine = makePage({
|
|
136
|
-
id: 'hvac-irvine',
|
|
137
|
-
pageType: 'location',
|
|
138
|
-
vertical: 'hvac-contractors',
|
|
139
|
-
city: 'irvine',
|
|
140
|
-
state: 'CA',
|
|
141
|
-
slug: 'ai-automation/hvac-contractors/irvine',
|
|
142
|
-
title: 'AI Automation for HVAC Contractors in Irvine'
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
const autoIrvine = makePage({
|
|
146
|
-
id: 'auto-irvine',
|
|
147
|
-
pageType: 'location',
|
|
148
|
-
vertical: 'auto-repair',
|
|
149
|
-
city: 'irvine',
|
|
150
|
-
state: 'CA',
|
|
151
|
-
slug: 'ai-automation/auto-repair/irvine',
|
|
152
|
-
title: 'AI Automation for Auto Repair Shops in Irvine'
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
const pestIrvine = makePage({
|
|
156
|
-
id: 'pest-irvine',
|
|
157
|
-
pageType: 'location',
|
|
158
|
-
vertical: 'pest-control',
|
|
159
|
-
city: 'irvine',
|
|
160
|
-
state: 'CA',
|
|
161
|
-
slug: 'ai-automation/pest-control/irvine',
|
|
162
|
-
title: 'AI Automation for Pest Control in Irvine'
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
// Cluster / use-case pages
|
|
166
|
-
const vetScheduling = makePage({
|
|
167
|
-
id: 'vet-scheduling',
|
|
168
|
-
pageType: 'cluster',
|
|
169
|
-
vertical: 'veterinary-clinics',
|
|
170
|
-
useCase: 'ai-scheduling',
|
|
171
|
-
city: null,
|
|
172
|
-
slug: 'solutions/ai-scheduling/veterinary-clinics',
|
|
173
|
-
title: 'AI Scheduling for Vet Clinics'
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
const vetReminders = makePage({
|
|
177
|
-
id: 'vet-reminders',
|
|
178
|
-
pageType: 'use-case',
|
|
179
|
-
vertical: 'veterinary-clinics',
|
|
180
|
-
useCase: 'appointment-reminders',
|
|
181
|
-
city: null,
|
|
182
|
-
slug: 'solutions/appointment-reminders/veterinary-clinics',
|
|
183
|
-
title: 'Appointment Reminders for Vet Clinics'
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
const vetIntake = makePage({
|
|
187
|
-
id: 'vet-intake',
|
|
188
|
-
pageType: 'cluster',
|
|
189
|
-
vertical: 'veterinary-clinics',
|
|
190
|
-
useCase: 'digital-intake',
|
|
191
|
-
city: null,
|
|
192
|
-
slug: 'solutions/digital-intake/veterinary-clinics',
|
|
193
|
-
title: 'Digital Intake for Vet Clinics'
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
// =============================================================================
|
|
197
|
-
// Anchor Text
|
|
198
|
-
// =============================================================================
|
|
199
|
-
|
|
200
|
-
describe('getVerticalLabel', () => {
|
|
201
|
-
it('returns known vertical labels', () => {
|
|
202
|
-
expect(getVerticalLabel('veterinary-clinics')).toBe('Vet Clinics')
|
|
203
|
-
expect(getVerticalLabel('hvac-contractors')).toBe('HVAC Contractors')
|
|
204
|
-
expect(getVerticalLabel('hoa-property-management')).toBe('HOA & Property Management')
|
|
205
|
-
})
|
|
206
|
-
|
|
207
|
-
it('falls back to title-cased slug for unknown verticals', () => {
|
|
208
|
-
expect(getVerticalLabel('new-vertical-type')).toBe('New Vertical Type')
|
|
209
|
-
})
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
describe('generateAnchorText', () => {
|
|
213
|
-
it('generates pillar anchor text', () => {
|
|
214
|
-
expect(generateAnchorText(vetPillar)).toBe('AI Automation for Vet Clinics')
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
it('generates location anchor text with city', () => {
|
|
218
|
-
expect(generateAnchorText(vetIrvine)).toBe('AI Automation for Vet Clinics in Irvine')
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
it('generates location anchor text with hyphenated city', () => {
|
|
222
|
-
expect(generateAnchorText(vetLosAngeles)).toBe('AI Automation for Vet Clinics in Los Angeles')
|
|
223
|
-
})
|
|
224
|
-
|
|
225
|
-
it('generates cluster anchor text with use-case', () => {
|
|
226
|
-
expect(generateAnchorText(vetScheduling)).toBe('Ai Scheduling for Vet Clinics')
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
it('falls back to page title when use-case is missing on cluster page', () => {
|
|
230
|
-
const noUseCase = makePage({
|
|
231
|
-
...vetScheduling,
|
|
232
|
-
id: 'no-use-case',
|
|
233
|
-
useCase: null,
|
|
234
|
-
slug: 'solutions/misc/veterinary-clinics',
|
|
235
|
-
title: 'Misc Page Title'
|
|
236
|
-
})
|
|
237
|
-
expect(generateAnchorText(noUseCase)).toBe('Misc Page Title')
|
|
238
|
-
})
|
|
239
|
-
})
|
|
240
|
-
|
|
241
|
-
// =============================================================================
|
|
242
|
-
// Pillar Page Linking
|
|
243
|
-
// =============================================================================
|
|
244
|
-
|
|
245
|
-
describe('generateInternalLinks — pillar page', () => {
|
|
246
|
-
const allPages = [
|
|
247
|
-
vetPillar,
|
|
248
|
-
hvacPillar,
|
|
249
|
-
autoPillar,
|
|
250
|
-
pestPillar,
|
|
251
|
-
vetIrvine,
|
|
252
|
-
vetAnaheim,
|
|
253
|
-
vetLosAngeles,
|
|
254
|
-
vetScheduling,
|
|
255
|
-
vetReminders,
|
|
256
|
-
vetIntake,
|
|
257
|
-
hvacIrvine
|
|
258
|
-
]
|
|
259
|
-
|
|
260
|
-
it('links to all location pages for its vertical', () => {
|
|
261
|
-
const links = generateInternalLinks(vetPillar, allPages)
|
|
262
|
-
const slugs = links.map((l) => l.slug)
|
|
263
|
-
|
|
264
|
-
expect(slugs).toContain(vetIrvine.slug)
|
|
265
|
-
expect(slugs).toContain(vetAnaheim.slug)
|
|
266
|
-
expect(slugs).toContain(vetLosAngeles.slug)
|
|
267
|
-
})
|
|
268
|
-
|
|
269
|
-
it('links to all cluster/use-case pages for its vertical', () => {
|
|
270
|
-
const links = generateInternalLinks(vetPillar, allPages)
|
|
271
|
-
const slugs = links.map((l) => l.slug)
|
|
272
|
-
|
|
273
|
-
expect(slugs).toContain(vetScheduling.slug)
|
|
274
|
-
expect(slugs).toContain(vetReminders.slug)
|
|
275
|
-
expect(slugs).toContain(vetIntake.slug)
|
|
276
|
-
})
|
|
277
|
-
|
|
278
|
-
it('links to up to 3 other vertical pillar pages', () => {
|
|
279
|
-
const links = generateInternalLinks(vetPillar, allPages)
|
|
280
|
-
const otherPillarLinks = links.filter((l) => [hvacPillar.slug, autoPillar.slug, pestPillar.slug].includes(l.slug))
|
|
281
|
-
expect(otherPillarLinks.length).toBeGreaterThanOrEqual(1)
|
|
282
|
-
expect(otherPillarLinks.length).toBeLessThanOrEqual(3)
|
|
283
|
-
})
|
|
284
|
-
|
|
285
|
-
it('does not link to itself', () => {
|
|
286
|
-
const links = generateInternalLinks(vetPillar, allPages)
|
|
287
|
-
const slugs = links.map((l) => l.slug)
|
|
288
|
-
expect(slugs).not.toContain(vetPillar.slug)
|
|
289
|
-
})
|
|
290
|
-
|
|
291
|
-
it('does not link to location pages from other verticals', () => {
|
|
292
|
-
const links = generateInternalLinks(vetPillar, allPages)
|
|
293
|
-
const slugs = links.map((l) => l.slug)
|
|
294
|
-
expect(slugs).not.toContain(hvacIrvine.slug)
|
|
295
|
-
})
|
|
296
|
-
|
|
297
|
-
it('returns an empty list when there are no other pages', () => {
|
|
298
|
-
const links = generateInternalLinks(vetPillar, [vetPillar])
|
|
299
|
-
expect(links).toHaveLength(0)
|
|
300
|
-
})
|
|
301
|
-
})
|
|
302
|
-
|
|
303
|
-
// =============================================================================
|
|
304
|
-
// Location Page Linking
|
|
305
|
-
// =============================================================================
|
|
306
|
-
|
|
307
|
-
describe('generateInternalLinks — location page', () => {
|
|
308
|
-
const allPages = [
|
|
309
|
-
vetPillar,
|
|
310
|
-
hvacPillar,
|
|
311
|
-
autoPillar,
|
|
312
|
-
pestPillar,
|
|
313
|
-
vetIrvine,
|
|
314
|
-
vetAnaheim,
|
|
315
|
-
vetLosAngeles,
|
|
316
|
-
vetSantaAna,
|
|
317
|
-
vetSanDiego,
|
|
318
|
-
vetSanFrancisco,
|
|
319
|
-
hvacIrvine,
|
|
320
|
-
autoIrvine,
|
|
321
|
-
pestIrvine,
|
|
322
|
-
vetScheduling,
|
|
323
|
-
vetReminders,
|
|
324
|
-
vetIntake
|
|
325
|
-
]
|
|
326
|
-
|
|
327
|
-
it('links back to its parent pillar page', () => {
|
|
328
|
-
const links = generateInternalLinks(vetIrvine, allPages)
|
|
329
|
-
const slugs = links.map((l) => l.slug)
|
|
330
|
-
expect(slugs).toContain(vetPillar.slug)
|
|
331
|
-
})
|
|
332
|
-
|
|
333
|
-
it('links to 3–5 sibling location pages', () => {
|
|
334
|
-
const links = generateInternalLinks(vetIrvine, allPages)
|
|
335
|
-
const siblingLinks = links.filter((l) =>
|
|
336
|
-
[vetAnaheim.slug, vetLosAngeles.slug, vetSantaAna.slug, vetSanDiego.slug, vetSanFrancisco.slug].includes(l.slug)
|
|
337
|
-
)
|
|
338
|
-
expect(siblingLinks.length).toBeGreaterThanOrEqual(3)
|
|
339
|
-
expect(siblingLinks.length).toBeLessThanOrEqual(5)
|
|
340
|
-
})
|
|
341
|
-
|
|
342
|
-
it('does not link to itself', () => {
|
|
343
|
-
const links = generateInternalLinks(vetIrvine, allPages)
|
|
344
|
-
const slugs = links.map((l) => l.slug)
|
|
345
|
-
expect(slugs).not.toContain(vetIrvine.slug)
|
|
346
|
-
})
|
|
347
|
-
|
|
348
|
-
it('links to cross-vertical pages in the same city', () => {
|
|
349
|
-
const links = generateInternalLinks(vetIrvine, allPages)
|
|
350
|
-
|
|
351
|
-
// hvac, auto, and pest all have Irvine pages — up to 3 should appear
|
|
352
|
-
const crossVerticalSlugs = [hvacIrvine.slug, autoIrvine.slug, pestIrvine.slug]
|
|
353
|
-
const crossLinks = links.filter((l) => crossVerticalSlugs.includes(l.slug))
|
|
354
|
-
expect(crossLinks.length).toBeGreaterThanOrEqual(1)
|
|
355
|
-
expect(crossLinks.length).toBeLessThanOrEqual(3)
|
|
356
|
-
})
|
|
357
|
-
|
|
358
|
-
it('does not link to cross-vertical pages in a different city', () => {
|
|
359
|
-
// hvacIrvine is in Irvine, vetAnaheim is in Anaheim — no cross link expected for anaheim page to hvacIrvine
|
|
360
|
-
const links = generateInternalLinks(vetAnaheim, allPages)
|
|
361
|
-
const slugs = links.map((l) => l.slug)
|
|
362
|
-
expect(slugs).not.toContain(hvacIrvine.slug)
|
|
363
|
-
})
|
|
364
|
-
|
|
365
|
-
it('links to 1–2 cluster pages', () => {
|
|
366
|
-
const links = generateInternalLinks(vetIrvine, allPages)
|
|
367
|
-
const clusterSlugs = [vetScheduling.slug, vetReminders.slug, vetIntake.slug]
|
|
368
|
-
const clusterLinks = links.filter((l) => clusterSlugs.includes(l.slug))
|
|
369
|
-
expect(clusterLinks.length).toBeGreaterThanOrEqual(1)
|
|
370
|
-
expect(clusterLinks.length).toBeLessThanOrEqual(2)
|
|
371
|
-
})
|
|
372
|
-
|
|
373
|
-
it('respects sibling limit of 5', () => {
|
|
374
|
-
// 5 sibling pages + many cross-vertical + clusters — ensure siblings are capped at 5
|
|
375
|
-
const links = generateInternalLinks(vetIrvine, allPages)
|
|
376
|
-
const siblingLinks = links.filter((l) =>
|
|
377
|
-
[vetAnaheim.slug, vetLosAngeles.slug, vetSantaAna.slug, vetSanDiego.slug, vetSanFrancisco.slug].includes(l.slug)
|
|
378
|
-
)
|
|
379
|
-
expect(siblingLinks.length).toBeLessThanOrEqual(5)
|
|
380
|
-
})
|
|
381
|
-
})
|
|
382
|
-
|
|
383
|
-
// =============================================================================
|
|
384
|
-
// Cluster / Use-case Page Linking
|
|
385
|
-
// =============================================================================
|
|
386
|
-
|
|
387
|
-
describe('generateInternalLinks — cluster/use-case page', () => {
|
|
388
|
-
const allPages = [vetPillar, vetIrvine, vetAnaheim, vetLosAngeles, vetScheduling, vetReminders, vetIntake]
|
|
389
|
-
|
|
390
|
-
it('links back to its parent pillar page', () => {
|
|
391
|
-
const links = generateInternalLinks(vetScheduling, allPages)
|
|
392
|
-
const slugs = links.map((l) => l.slug)
|
|
393
|
-
expect(slugs).toContain(vetPillar.slug)
|
|
394
|
-
})
|
|
395
|
-
|
|
396
|
-
it('links to location pages for the same vertical', () => {
|
|
397
|
-
const links = generateInternalLinks(vetScheduling, allPages)
|
|
398
|
-
const locationSlugs = [vetIrvine.slug, vetAnaheim.slug, vetLosAngeles.slug]
|
|
399
|
-
const locationLinks = links.filter((l) => locationSlugs.includes(l.slug))
|
|
400
|
-
expect(locationLinks.length).toBeGreaterThanOrEqual(1)
|
|
401
|
-
expect(locationLinks.length).toBeLessThanOrEqual(3)
|
|
402
|
-
})
|
|
403
|
-
|
|
404
|
-
it('links to related cluster pages (same vertical, different slug)', () => {
|
|
405
|
-
const links = generateInternalLinks(vetScheduling, allPages)
|
|
406
|
-
const slugs = links.map((l) => l.slug)
|
|
407
|
-
|
|
408
|
-
expect(slugs).not.toContain(vetScheduling.slug)
|
|
409
|
-
const relatedClusters = [vetReminders.slug, vetIntake.slug]
|
|
410
|
-
const clusterLinks = links.filter((l) => relatedClusters.includes(l.slug))
|
|
411
|
-
expect(clusterLinks.length).toBeGreaterThanOrEqual(1)
|
|
412
|
-
expect(clusterLinks.length).toBeLessThanOrEqual(3)
|
|
413
|
-
})
|
|
414
|
-
|
|
415
|
-
it('does not link to itself', () => {
|
|
416
|
-
const links = generateInternalLinks(vetScheduling, allPages)
|
|
417
|
-
const slugs = links.map((l) => l.slug)
|
|
418
|
-
expect(slugs).not.toContain(vetScheduling.slug)
|
|
419
|
-
})
|
|
420
|
-
|
|
421
|
-
it('works for use-case page type as well', () => {
|
|
422
|
-
const links = generateInternalLinks(vetReminders, allPages)
|
|
423
|
-
const slugs = links.map((l) => l.slug)
|
|
424
|
-
expect(slugs).toContain(vetPillar.slug)
|
|
425
|
-
expect(slugs).not.toContain(vetReminders.slug)
|
|
426
|
-
})
|
|
427
|
-
})
|
|
428
|
-
|
|
429
|
-
// =============================================================================
|
|
430
|
-
// Comparison / Other Page Types
|
|
431
|
-
// =============================================================================
|
|
432
|
-
|
|
433
|
-
describe('generateInternalLinks — comparison page', () => {
|
|
434
|
-
it('returns an empty list (no hub-and-spoke rules defined)', () => {
|
|
435
|
-
const compPage = makePage({
|
|
436
|
-
id: 'comp-1',
|
|
437
|
-
pageType: 'comparison',
|
|
438
|
-
slug: 'compare/veterinary-clinics',
|
|
439
|
-
vertical: 'veterinary-clinics',
|
|
440
|
-
city: null,
|
|
441
|
-
title: 'Compare AI Automation for Vet Clinics'
|
|
442
|
-
})
|
|
443
|
-
const links = generateInternalLinks(compPage, [vetPillar, vetIrvine, vetScheduling])
|
|
444
|
-
expect(links).toHaveLength(0)
|
|
445
|
-
})
|
|
446
|
-
})
|
|
447
|
-
|
|
448
|
-
// =============================================================================
|
|
449
|
-
// Link Count Limits
|
|
450
|
-
// =============================================================================
|
|
451
|
-
|
|
452
|
-
describe('link count limits', () => {
|
|
453
|
-
it('pillar page respects OTHER_VERTICALS limit of 3', () => {
|
|
454
|
-
// 6 other verticals — only 3 should be picked
|
|
455
|
-
const manyPillars = [
|
|
456
|
-
vetPillar,
|
|
457
|
-
hvacPillar,
|
|
458
|
-
autoPillar,
|
|
459
|
-
pestPillar,
|
|
460
|
-
makePage({
|
|
461
|
-
id: 'clean-pillar',
|
|
462
|
-
pageType: 'pillar',
|
|
463
|
-
vertical: 'commercial-cleaning',
|
|
464
|
-
city: null,
|
|
465
|
-
slug: 'ai-automation/commercial-cleaning',
|
|
466
|
-
title: 'AI Automation for Commercial Cleaning'
|
|
467
|
-
}),
|
|
468
|
-
makePage({
|
|
469
|
-
id: 'spa-pillar',
|
|
470
|
-
pageType: 'pillar',
|
|
471
|
-
vertical: 'med-spas',
|
|
472
|
-
city: null,
|
|
473
|
-
slug: 'ai-automation/med-spas',
|
|
474
|
-
title: 'AI Automation for Med Spas'
|
|
475
|
-
}),
|
|
476
|
-
makePage({
|
|
477
|
-
id: 'hoa-pillar',
|
|
478
|
-
pageType: 'pillar',
|
|
479
|
-
vertical: 'hoa-property-management',
|
|
480
|
-
city: null,
|
|
481
|
-
slug: 'ai-automation/hoa-property-management',
|
|
482
|
-
title: 'AI Automation for HOA'
|
|
483
|
-
})
|
|
484
|
-
]
|
|
485
|
-
const links = generateInternalLinks(vetPillar, manyPillars)
|
|
486
|
-
const otherPillarLinks = links.filter((l) => l.slug !== vetPillar.slug)
|
|
487
|
-
expect(otherPillarLinks.length).toBeLessThanOrEqual(3)
|
|
488
|
-
})
|
|
489
|
-
|
|
490
|
-
it('location page sibling count does not exceed 5', () => {
|
|
491
|
-
// Create 10 sibling pages
|
|
492
|
-
const manySiblings: SeoPage[] = Array.from({ length: 10 }, (_, i) =>
|
|
493
|
-
makePage({
|
|
494
|
-
id: `vet-city-${i}`,
|
|
495
|
-
pageType: 'location',
|
|
496
|
-
vertical: 'veterinary-clinics',
|
|
497
|
-
city: `city-${i}`,
|
|
498
|
-
state: 'CA',
|
|
499
|
-
slug: `ai-automation/veterinary-clinics/city-${i}`,
|
|
500
|
-
title: `AI Automation for Vet Clinics in City ${i}`
|
|
501
|
-
})
|
|
502
|
-
)
|
|
503
|
-
|
|
504
|
-
const targetPage = manySiblings[0]!
|
|
505
|
-
const links = generateInternalLinks(targetPage, [vetPillar, ...manySiblings])
|
|
506
|
-
const siblingLinks = links.filter((l) => l.slug !== vetPillar.slug && l.slug !== targetPage.slug)
|
|
507
|
-
expect(siblingLinks.length).toBeLessThanOrEqual(5)
|
|
508
|
-
})
|
|
509
|
-
|
|
510
|
-
it('cluster page location links do not exceed 3', () => {
|
|
511
|
-
const manyLocations: SeoPage[] = Array.from({ length: 8 }, (_, i) =>
|
|
512
|
-
makePage({
|
|
513
|
-
id: `vet-loc-${i}`,
|
|
514
|
-
pageType: 'location',
|
|
515
|
-
vertical: 'veterinary-clinics',
|
|
516
|
-
city: `city-${i}`,
|
|
517
|
-
state: 'CA',
|
|
518
|
-
slug: `ai-automation/veterinary-clinics/city-${i}`,
|
|
519
|
-
title: `Vet Clinics in City ${i}`
|
|
520
|
-
})
|
|
521
|
-
)
|
|
522
|
-
|
|
523
|
-
const links = generateInternalLinks(vetScheduling, [vetPillar, ...manyLocations, vetScheduling])
|
|
524
|
-
const locationLinks = links.filter((l) => manyLocations.some((loc) => loc.slug === l.slug))
|
|
525
|
-
expect(locationLinks.length).toBeLessThanOrEqual(3)
|
|
526
|
-
})
|
|
527
|
-
})
|
|
528
|
-
|
|
529
|
-
// =============================================================================
|
|
530
|
-
// Determinism
|
|
531
|
-
// =============================================================================
|
|
532
|
-
|
|
533
|
-
describe('determinism', () => {
|
|
534
|
-
it('generates the same links for the same input on multiple calls', () => {
|
|
535
|
-
const allPages = [
|
|
536
|
-
vetPillar,
|
|
537
|
-
hvacPillar,
|
|
538
|
-
autoPillar,
|
|
539
|
-
vetIrvine,
|
|
540
|
-
vetAnaheim,
|
|
541
|
-
vetLosAngeles,
|
|
542
|
-
vetScheduling,
|
|
543
|
-
vetReminders
|
|
544
|
-
]
|
|
545
|
-
const first = generateInternalLinks(vetIrvine, allPages)
|
|
546
|
-
const second = generateInternalLinks(vetIrvine, allPages)
|
|
547
|
-
expect(first).toEqual(second)
|
|
548
|
-
})
|
|
549
|
-
})
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { generateInternalLinks, generateAnchorText, getVerticalLabel } from '../linking'
|
|
3
|
+
import type { SeoPage } from '../types'
|
|
4
|
+
|
|
5
|
+
// =============================================================================
|
|
6
|
+
// Fixtures
|
|
7
|
+
// =============================================================================
|
|
8
|
+
|
|
9
|
+
const BASE_PAGE: SeoPage = {
|
|
10
|
+
id: '00000000-0000-0000-0000-000000000000',
|
|
11
|
+
organizationId: 'org-1',
|
|
12
|
+
pageType: 'pillar',
|
|
13
|
+
vertical: 'veterinary-clinics',
|
|
14
|
+
city: null,
|
|
15
|
+
state: null,
|
|
16
|
+
useCase: null,
|
|
17
|
+
slug: 'ai-automation/veterinary-clinics',
|
|
18
|
+
title: 'AI Automation for Veterinary Clinics',
|
|
19
|
+
metaDescription: null,
|
|
20
|
+
content: null,
|
|
21
|
+
faqItems: null,
|
|
22
|
+
schemaMarkup: null,
|
|
23
|
+
localData: null,
|
|
24
|
+
internalLinks: null,
|
|
25
|
+
status: 'published',
|
|
26
|
+
publishedAt: new Date('2026-01-01'),
|
|
27
|
+
refreshedAt: null,
|
|
28
|
+
createdAt: new Date('2026-01-01'),
|
|
29
|
+
updatedAt: new Date('2026-01-01')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function makePage(overrides: Partial<SeoPage> & { id: string; slug: string }): SeoPage {
|
|
33
|
+
return { ...BASE_PAGE, ...overrides }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Pillar pages for two verticals
|
|
37
|
+
const vetPillar = makePage({
|
|
38
|
+
id: 'vet-pillar',
|
|
39
|
+
pageType: 'pillar',
|
|
40
|
+
vertical: 'veterinary-clinics',
|
|
41
|
+
city: null,
|
|
42
|
+
slug: 'ai-automation/veterinary-clinics',
|
|
43
|
+
title: 'AI Automation for Veterinary Clinics'
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const hvacPillar = makePage({
|
|
47
|
+
id: 'hvac-pillar',
|
|
48
|
+
pageType: 'pillar',
|
|
49
|
+
vertical: 'hvac-contractors',
|
|
50
|
+
city: null,
|
|
51
|
+
slug: 'ai-automation/hvac-contractors',
|
|
52
|
+
title: 'AI Automation for HVAC Contractors'
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const autoPillar = makePage({
|
|
56
|
+
id: 'auto-pillar',
|
|
57
|
+
pageType: 'pillar',
|
|
58
|
+
vertical: 'auto-repair',
|
|
59
|
+
city: null,
|
|
60
|
+
slug: 'ai-automation/auto-repair',
|
|
61
|
+
title: 'AI Automation for Auto Repair Shops'
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const pestPillar = makePage({
|
|
65
|
+
id: 'pest-pillar',
|
|
66
|
+
pageType: 'pillar',
|
|
67
|
+
vertical: 'pest-control',
|
|
68
|
+
city: null,
|
|
69
|
+
slug: 'ai-automation/pest-control',
|
|
70
|
+
title: 'AI Automation for Pest Control'
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// Location pages
|
|
74
|
+
const vetIrvine = makePage({
|
|
75
|
+
id: 'vet-irvine',
|
|
76
|
+
pageType: 'location',
|
|
77
|
+
vertical: 'veterinary-clinics',
|
|
78
|
+
city: 'irvine',
|
|
79
|
+
state: 'CA',
|
|
80
|
+
slug: 'ai-automation/veterinary-clinics/irvine',
|
|
81
|
+
title: 'AI Automation for Vet Clinics in Irvine'
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const vetAnaheim = makePage({
|
|
85
|
+
id: 'vet-anaheim',
|
|
86
|
+
pageType: 'location',
|
|
87
|
+
vertical: 'veterinary-clinics',
|
|
88
|
+
city: 'anaheim',
|
|
89
|
+
state: 'CA',
|
|
90
|
+
slug: 'ai-automation/veterinary-clinics/anaheim',
|
|
91
|
+
title: 'AI Automation for Vet Clinics in Anaheim'
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const vetLosAngeles = makePage({
|
|
95
|
+
id: 'vet-la',
|
|
96
|
+
pageType: 'location',
|
|
97
|
+
vertical: 'veterinary-clinics',
|
|
98
|
+
city: 'los-angeles',
|
|
99
|
+
state: 'CA',
|
|
100
|
+
slug: 'ai-automation/veterinary-clinics/los-angeles',
|
|
101
|
+
title: 'AI Automation for Vet Clinics in Los Angeles'
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const vetSantaAna = makePage({
|
|
105
|
+
id: 'vet-santa-ana',
|
|
106
|
+
pageType: 'location',
|
|
107
|
+
vertical: 'veterinary-clinics',
|
|
108
|
+
city: 'santa-ana',
|
|
109
|
+
state: 'CA',
|
|
110
|
+
slug: 'ai-automation/veterinary-clinics/santa-ana',
|
|
111
|
+
title: 'AI Automation for Vet Clinics in Santa Ana'
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const vetSanDiego = makePage({
|
|
115
|
+
id: 'vet-san-diego',
|
|
116
|
+
pageType: 'location',
|
|
117
|
+
vertical: 'veterinary-clinics',
|
|
118
|
+
city: 'san-diego',
|
|
119
|
+
state: 'CA',
|
|
120
|
+
slug: 'ai-automation/veterinary-clinics/san-diego',
|
|
121
|
+
title: 'AI Automation for Vet Clinics in San Diego'
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
const vetSanFrancisco = makePage({
|
|
125
|
+
id: 'vet-sf',
|
|
126
|
+
pageType: 'location',
|
|
127
|
+
vertical: 'veterinary-clinics',
|
|
128
|
+
city: 'san-francisco',
|
|
129
|
+
state: 'CA',
|
|
130
|
+
slug: 'ai-automation/veterinary-clinics/san-francisco',
|
|
131
|
+
title: 'AI Automation for Vet Clinics in San Francisco'
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
// Cross-vertical location page in same city as vetIrvine
|
|
135
|
+
const hvacIrvine = makePage({
|
|
136
|
+
id: 'hvac-irvine',
|
|
137
|
+
pageType: 'location',
|
|
138
|
+
vertical: 'hvac-contractors',
|
|
139
|
+
city: 'irvine',
|
|
140
|
+
state: 'CA',
|
|
141
|
+
slug: 'ai-automation/hvac-contractors/irvine',
|
|
142
|
+
title: 'AI Automation for HVAC Contractors in Irvine'
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
const autoIrvine = makePage({
|
|
146
|
+
id: 'auto-irvine',
|
|
147
|
+
pageType: 'location',
|
|
148
|
+
vertical: 'auto-repair',
|
|
149
|
+
city: 'irvine',
|
|
150
|
+
state: 'CA',
|
|
151
|
+
slug: 'ai-automation/auto-repair/irvine',
|
|
152
|
+
title: 'AI Automation for Auto Repair Shops in Irvine'
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
const pestIrvine = makePage({
|
|
156
|
+
id: 'pest-irvine',
|
|
157
|
+
pageType: 'location',
|
|
158
|
+
vertical: 'pest-control',
|
|
159
|
+
city: 'irvine',
|
|
160
|
+
state: 'CA',
|
|
161
|
+
slug: 'ai-automation/pest-control/irvine',
|
|
162
|
+
title: 'AI Automation for Pest Control in Irvine'
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
// Cluster / use-case pages
|
|
166
|
+
const vetScheduling = makePage({
|
|
167
|
+
id: 'vet-scheduling',
|
|
168
|
+
pageType: 'cluster',
|
|
169
|
+
vertical: 'veterinary-clinics',
|
|
170
|
+
useCase: 'ai-scheduling',
|
|
171
|
+
city: null,
|
|
172
|
+
slug: 'solutions/ai-scheduling/veterinary-clinics',
|
|
173
|
+
title: 'AI Scheduling for Vet Clinics'
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
const vetReminders = makePage({
|
|
177
|
+
id: 'vet-reminders',
|
|
178
|
+
pageType: 'use-case',
|
|
179
|
+
vertical: 'veterinary-clinics',
|
|
180
|
+
useCase: 'appointment-reminders',
|
|
181
|
+
city: null,
|
|
182
|
+
slug: 'solutions/appointment-reminders/veterinary-clinics',
|
|
183
|
+
title: 'Appointment Reminders for Vet Clinics'
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
const vetIntake = makePage({
|
|
187
|
+
id: 'vet-intake',
|
|
188
|
+
pageType: 'cluster',
|
|
189
|
+
vertical: 'veterinary-clinics',
|
|
190
|
+
useCase: 'digital-intake',
|
|
191
|
+
city: null,
|
|
192
|
+
slug: 'solutions/digital-intake/veterinary-clinics',
|
|
193
|
+
title: 'Digital Intake for Vet Clinics'
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
// =============================================================================
|
|
197
|
+
// Anchor Text
|
|
198
|
+
// =============================================================================
|
|
199
|
+
|
|
200
|
+
describe('getVerticalLabel', () => {
|
|
201
|
+
it('returns known vertical labels', () => {
|
|
202
|
+
expect(getVerticalLabel('veterinary-clinics')).toBe('Vet Clinics')
|
|
203
|
+
expect(getVerticalLabel('hvac-contractors')).toBe('HVAC Contractors')
|
|
204
|
+
expect(getVerticalLabel('hoa-property-management')).toBe('HOA & Property Management')
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('falls back to title-cased slug for unknown verticals', () => {
|
|
208
|
+
expect(getVerticalLabel('new-vertical-type')).toBe('New Vertical Type')
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
describe('generateAnchorText', () => {
|
|
213
|
+
it('generates pillar anchor text', () => {
|
|
214
|
+
expect(generateAnchorText(vetPillar)).toBe('AI Automation for Vet Clinics')
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('generates location anchor text with city', () => {
|
|
218
|
+
expect(generateAnchorText(vetIrvine)).toBe('AI Automation for Vet Clinics in Irvine')
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('generates location anchor text with hyphenated city', () => {
|
|
222
|
+
expect(generateAnchorText(vetLosAngeles)).toBe('AI Automation for Vet Clinics in Los Angeles')
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('generates cluster anchor text with use-case', () => {
|
|
226
|
+
expect(generateAnchorText(vetScheduling)).toBe('Ai Scheduling for Vet Clinics')
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('falls back to page title when use-case is missing on cluster page', () => {
|
|
230
|
+
const noUseCase = makePage({
|
|
231
|
+
...vetScheduling,
|
|
232
|
+
id: 'no-use-case',
|
|
233
|
+
useCase: null,
|
|
234
|
+
slug: 'solutions/misc/veterinary-clinics',
|
|
235
|
+
title: 'Misc Page Title'
|
|
236
|
+
})
|
|
237
|
+
expect(generateAnchorText(noUseCase)).toBe('Misc Page Title')
|
|
238
|
+
})
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
// =============================================================================
|
|
242
|
+
// Pillar Page Linking
|
|
243
|
+
// =============================================================================
|
|
244
|
+
|
|
245
|
+
describe('generateInternalLinks — pillar page', () => {
|
|
246
|
+
const allPages = [
|
|
247
|
+
vetPillar,
|
|
248
|
+
hvacPillar,
|
|
249
|
+
autoPillar,
|
|
250
|
+
pestPillar,
|
|
251
|
+
vetIrvine,
|
|
252
|
+
vetAnaheim,
|
|
253
|
+
vetLosAngeles,
|
|
254
|
+
vetScheduling,
|
|
255
|
+
vetReminders,
|
|
256
|
+
vetIntake,
|
|
257
|
+
hvacIrvine
|
|
258
|
+
]
|
|
259
|
+
|
|
260
|
+
it('links to all location pages for its vertical', () => {
|
|
261
|
+
const links = generateInternalLinks(vetPillar, allPages)
|
|
262
|
+
const slugs = links.map((l) => l.slug)
|
|
263
|
+
|
|
264
|
+
expect(slugs).toContain(vetIrvine.slug)
|
|
265
|
+
expect(slugs).toContain(vetAnaheim.slug)
|
|
266
|
+
expect(slugs).toContain(vetLosAngeles.slug)
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('links to all cluster/use-case pages for its vertical', () => {
|
|
270
|
+
const links = generateInternalLinks(vetPillar, allPages)
|
|
271
|
+
const slugs = links.map((l) => l.slug)
|
|
272
|
+
|
|
273
|
+
expect(slugs).toContain(vetScheduling.slug)
|
|
274
|
+
expect(slugs).toContain(vetReminders.slug)
|
|
275
|
+
expect(slugs).toContain(vetIntake.slug)
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('links to up to 3 other vertical pillar pages', () => {
|
|
279
|
+
const links = generateInternalLinks(vetPillar, allPages)
|
|
280
|
+
const otherPillarLinks = links.filter((l) => [hvacPillar.slug, autoPillar.slug, pestPillar.slug].includes(l.slug))
|
|
281
|
+
expect(otherPillarLinks.length).toBeGreaterThanOrEqual(1)
|
|
282
|
+
expect(otherPillarLinks.length).toBeLessThanOrEqual(3)
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it('does not link to itself', () => {
|
|
286
|
+
const links = generateInternalLinks(vetPillar, allPages)
|
|
287
|
+
const slugs = links.map((l) => l.slug)
|
|
288
|
+
expect(slugs).not.toContain(vetPillar.slug)
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
it('does not link to location pages from other verticals', () => {
|
|
292
|
+
const links = generateInternalLinks(vetPillar, allPages)
|
|
293
|
+
const slugs = links.map((l) => l.slug)
|
|
294
|
+
expect(slugs).not.toContain(hvacIrvine.slug)
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it('returns an empty list when there are no other pages', () => {
|
|
298
|
+
const links = generateInternalLinks(vetPillar, [vetPillar])
|
|
299
|
+
expect(links).toHaveLength(0)
|
|
300
|
+
})
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
// =============================================================================
|
|
304
|
+
// Location Page Linking
|
|
305
|
+
// =============================================================================
|
|
306
|
+
|
|
307
|
+
describe('generateInternalLinks — location page', () => {
|
|
308
|
+
const allPages = [
|
|
309
|
+
vetPillar,
|
|
310
|
+
hvacPillar,
|
|
311
|
+
autoPillar,
|
|
312
|
+
pestPillar,
|
|
313
|
+
vetIrvine,
|
|
314
|
+
vetAnaheim,
|
|
315
|
+
vetLosAngeles,
|
|
316
|
+
vetSantaAna,
|
|
317
|
+
vetSanDiego,
|
|
318
|
+
vetSanFrancisco,
|
|
319
|
+
hvacIrvine,
|
|
320
|
+
autoIrvine,
|
|
321
|
+
pestIrvine,
|
|
322
|
+
vetScheduling,
|
|
323
|
+
vetReminders,
|
|
324
|
+
vetIntake
|
|
325
|
+
]
|
|
326
|
+
|
|
327
|
+
it('links back to its parent pillar page', () => {
|
|
328
|
+
const links = generateInternalLinks(vetIrvine, allPages)
|
|
329
|
+
const slugs = links.map((l) => l.slug)
|
|
330
|
+
expect(slugs).toContain(vetPillar.slug)
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
it('links to 3–5 sibling location pages', () => {
|
|
334
|
+
const links = generateInternalLinks(vetIrvine, allPages)
|
|
335
|
+
const siblingLinks = links.filter((l) =>
|
|
336
|
+
[vetAnaheim.slug, vetLosAngeles.slug, vetSantaAna.slug, vetSanDiego.slug, vetSanFrancisco.slug].includes(l.slug)
|
|
337
|
+
)
|
|
338
|
+
expect(siblingLinks.length).toBeGreaterThanOrEqual(3)
|
|
339
|
+
expect(siblingLinks.length).toBeLessThanOrEqual(5)
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
it('does not link to itself', () => {
|
|
343
|
+
const links = generateInternalLinks(vetIrvine, allPages)
|
|
344
|
+
const slugs = links.map((l) => l.slug)
|
|
345
|
+
expect(slugs).not.toContain(vetIrvine.slug)
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
it('links to cross-vertical pages in the same city', () => {
|
|
349
|
+
const links = generateInternalLinks(vetIrvine, allPages)
|
|
350
|
+
|
|
351
|
+
// hvac, auto, and pest all have Irvine pages — up to 3 should appear
|
|
352
|
+
const crossVerticalSlugs = [hvacIrvine.slug, autoIrvine.slug, pestIrvine.slug]
|
|
353
|
+
const crossLinks = links.filter((l) => crossVerticalSlugs.includes(l.slug))
|
|
354
|
+
expect(crossLinks.length).toBeGreaterThanOrEqual(1)
|
|
355
|
+
expect(crossLinks.length).toBeLessThanOrEqual(3)
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
it('does not link to cross-vertical pages in a different city', () => {
|
|
359
|
+
// hvacIrvine is in Irvine, vetAnaheim is in Anaheim — no cross link expected for anaheim page to hvacIrvine
|
|
360
|
+
const links = generateInternalLinks(vetAnaheim, allPages)
|
|
361
|
+
const slugs = links.map((l) => l.slug)
|
|
362
|
+
expect(slugs).not.toContain(hvacIrvine.slug)
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
it('links to 1–2 cluster pages', () => {
|
|
366
|
+
const links = generateInternalLinks(vetIrvine, allPages)
|
|
367
|
+
const clusterSlugs = [vetScheduling.slug, vetReminders.slug, vetIntake.slug]
|
|
368
|
+
const clusterLinks = links.filter((l) => clusterSlugs.includes(l.slug))
|
|
369
|
+
expect(clusterLinks.length).toBeGreaterThanOrEqual(1)
|
|
370
|
+
expect(clusterLinks.length).toBeLessThanOrEqual(2)
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
it('respects sibling limit of 5', () => {
|
|
374
|
+
// 5 sibling pages + many cross-vertical + clusters — ensure siblings are capped at 5
|
|
375
|
+
const links = generateInternalLinks(vetIrvine, allPages)
|
|
376
|
+
const siblingLinks = links.filter((l) =>
|
|
377
|
+
[vetAnaheim.slug, vetLosAngeles.slug, vetSantaAna.slug, vetSanDiego.slug, vetSanFrancisco.slug].includes(l.slug)
|
|
378
|
+
)
|
|
379
|
+
expect(siblingLinks.length).toBeLessThanOrEqual(5)
|
|
380
|
+
})
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
// =============================================================================
|
|
384
|
+
// Cluster / Use-case Page Linking
|
|
385
|
+
// =============================================================================
|
|
386
|
+
|
|
387
|
+
describe('generateInternalLinks — cluster/use-case page', () => {
|
|
388
|
+
const allPages = [vetPillar, vetIrvine, vetAnaheim, vetLosAngeles, vetScheduling, vetReminders, vetIntake]
|
|
389
|
+
|
|
390
|
+
it('links back to its parent pillar page', () => {
|
|
391
|
+
const links = generateInternalLinks(vetScheduling, allPages)
|
|
392
|
+
const slugs = links.map((l) => l.slug)
|
|
393
|
+
expect(slugs).toContain(vetPillar.slug)
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
it('links to location pages for the same vertical', () => {
|
|
397
|
+
const links = generateInternalLinks(vetScheduling, allPages)
|
|
398
|
+
const locationSlugs = [vetIrvine.slug, vetAnaheim.slug, vetLosAngeles.slug]
|
|
399
|
+
const locationLinks = links.filter((l) => locationSlugs.includes(l.slug))
|
|
400
|
+
expect(locationLinks.length).toBeGreaterThanOrEqual(1)
|
|
401
|
+
expect(locationLinks.length).toBeLessThanOrEqual(3)
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
it('links to related cluster pages (same vertical, different slug)', () => {
|
|
405
|
+
const links = generateInternalLinks(vetScheduling, allPages)
|
|
406
|
+
const slugs = links.map((l) => l.slug)
|
|
407
|
+
|
|
408
|
+
expect(slugs).not.toContain(vetScheduling.slug)
|
|
409
|
+
const relatedClusters = [vetReminders.slug, vetIntake.slug]
|
|
410
|
+
const clusterLinks = links.filter((l) => relatedClusters.includes(l.slug))
|
|
411
|
+
expect(clusterLinks.length).toBeGreaterThanOrEqual(1)
|
|
412
|
+
expect(clusterLinks.length).toBeLessThanOrEqual(3)
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
it('does not link to itself', () => {
|
|
416
|
+
const links = generateInternalLinks(vetScheduling, allPages)
|
|
417
|
+
const slugs = links.map((l) => l.slug)
|
|
418
|
+
expect(slugs).not.toContain(vetScheduling.slug)
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
it('works for use-case page type as well', () => {
|
|
422
|
+
const links = generateInternalLinks(vetReminders, allPages)
|
|
423
|
+
const slugs = links.map((l) => l.slug)
|
|
424
|
+
expect(slugs).toContain(vetPillar.slug)
|
|
425
|
+
expect(slugs).not.toContain(vetReminders.slug)
|
|
426
|
+
})
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
// =============================================================================
|
|
430
|
+
// Comparison / Other Page Types
|
|
431
|
+
// =============================================================================
|
|
432
|
+
|
|
433
|
+
describe('generateInternalLinks — comparison page', () => {
|
|
434
|
+
it('returns an empty list (no hub-and-spoke rules defined)', () => {
|
|
435
|
+
const compPage = makePage({
|
|
436
|
+
id: 'comp-1',
|
|
437
|
+
pageType: 'comparison',
|
|
438
|
+
slug: 'compare/veterinary-clinics',
|
|
439
|
+
vertical: 'veterinary-clinics',
|
|
440
|
+
city: null,
|
|
441
|
+
title: 'Compare AI Automation for Vet Clinics'
|
|
442
|
+
})
|
|
443
|
+
const links = generateInternalLinks(compPage, [vetPillar, vetIrvine, vetScheduling])
|
|
444
|
+
expect(links).toHaveLength(0)
|
|
445
|
+
})
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
// =============================================================================
|
|
449
|
+
// Link Count Limits
|
|
450
|
+
// =============================================================================
|
|
451
|
+
|
|
452
|
+
describe('link count limits', () => {
|
|
453
|
+
it('pillar page respects OTHER_VERTICALS limit of 3', () => {
|
|
454
|
+
// 6 other verticals — only 3 should be picked
|
|
455
|
+
const manyPillars = [
|
|
456
|
+
vetPillar,
|
|
457
|
+
hvacPillar,
|
|
458
|
+
autoPillar,
|
|
459
|
+
pestPillar,
|
|
460
|
+
makePage({
|
|
461
|
+
id: 'clean-pillar',
|
|
462
|
+
pageType: 'pillar',
|
|
463
|
+
vertical: 'commercial-cleaning',
|
|
464
|
+
city: null,
|
|
465
|
+
slug: 'ai-automation/commercial-cleaning',
|
|
466
|
+
title: 'AI Automation for Commercial Cleaning'
|
|
467
|
+
}),
|
|
468
|
+
makePage({
|
|
469
|
+
id: 'spa-pillar',
|
|
470
|
+
pageType: 'pillar',
|
|
471
|
+
vertical: 'med-spas',
|
|
472
|
+
city: null,
|
|
473
|
+
slug: 'ai-automation/med-spas',
|
|
474
|
+
title: 'AI Automation for Med Spas'
|
|
475
|
+
}),
|
|
476
|
+
makePage({
|
|
477
|
+
id: 'hoa-pillar',
|
|
478
|
+
pageType: 'pillar',
|
|
479
|
+
vertical: 'hoa-property-management',
|
|
480
|
+
city: null,
|
|
481
|
+
slug: 'ai-automation/hoa-property-management',
|
|
482
|
+
title: 'AI Automation for HOA'
|
|
483
|
+
})
|
|
484
|
+
]
|
|
485
|
+
const links = generateInternalLinks(vetPillar, manyPillars)
|
|
486
|
+
const otherPillarLinks = links.filter((l) => l.slug !== vetPillar.slug)
|
|
487
|
+
expect(otherPillarLinks.length).toBeLessThanOrEqual(3)
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
it('location page sibling count does not exceed 5', () => {
|
|
491
|
+
// Create 10 sibling pages
|
|
492
|
+
const manySiblings: SeoPage[] = Array.from({ length: 10 }, (_, i) =>
|
|
493
|
+
makePage({
|
|
494
|
+
id: `vet-city-${i}`,
|
|
495
|
+
pageType: 'location',
|
|
496
|
+
vertical: 'veterinary-clinics',
|
|
497
|
+
city: `city-${i}`,
|
|
498
|
+
state: 'CA',
|
|
499
|
+
slug: `ai-automation/veterinary-clinics/city-${i}`,
|
|
500
|
+
title: `AI Automation for Vet Clinics in City ${i}`
|
|
501
|
+
})
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
const targetPage = manySiblings[0]!
|
|
505
|
+
const links = generateInternalLinks(targetPage, [vetPillar, ...manySiblings])
|
|
506
|
+
const siblingLinks = links.filter((l) => l.slug !== vetPillar.slug && l.slug !== targetPage.slug)
|
|
507
|
+
expect(siblingLinks.length).toBeLessThanOrEqual(5)
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
it('cluster page location links do not exceed 3', () => {
|
|
511
|
+
const manyLocations: SeoPage[] = Array.from({ length: 8 }, (_, i) =>
|
|
512
|
+
makePage({
|
|
513
|
+
id: `vet-loc-${i}`,
|
|
514
|
+
pageType: 'location',
|
|
515
|
+
vertical: 'veterinary-clinics',
|
|
516
|
+
city: `city-${i}`,
|
|
517
|
+
state: 'CA',
|
|
518
|
+
slug: `ai-automation/veterinary-clinics/city-${i}`,
|
|
519
|
+
title: `Vet Clinics in City ${i}`
|
|
520
|
+
})
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
const links = generateInternalLinks(vetScheduling, [vetPillar, ...manyLocations, vetScheduling])
|
|
524
|
+
const locationLinks = links.filter((l) => manyLocations.some((loc) => loc.slug === l.slug))
|
|
525
|
+
expect(locationLinks.length).toBeLessThanOrEqual(3)
|
|
526
|
+
})
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
// =============================================================================
|
|
530
|
+
// Determinism
|
|
531
|
+
// =============================================================================
|
|
532
|
+
|
|
533
|
+
describe('determinism', () => {
|
|
534
|
+
it('generates the same links for the same input on multiple calls', () => {
|
|
535
|
+
const allPages = [
|
|
536
|
+
vetPillar,
|
|
537
|
+
hvacPillar,
|
|
538
|
+
autoPillar,
|
|
539
|
+
vetIrvine,
|
|
540
|
+
vetAnaheim,
|
|
541
|
+
vetLosAngeles,
|
|
542
|
+
vetScheduling,
|
|
543
|
+
vetReminders
|
|
544
|
+
]
|
|
545
|
+
const first = generateInternalLinks(vetIrvine, allPages)
|
|
546
|
+
const second = generateInternalLinks(vetIrvine, allPages)
|
|
547
|
+
expect(first).toEqual(second)
|
|
548
|
+
})
|
|
549
|
+
})
|