@lunora/advisor 1.0.0-alpha.25 → 1.0.0-alpha.26

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/dist/index.d.mts CHANGED
@@ -2023,6 +2023,22 @@ interface AnalyticsRuntimeMetrics {
2023
2023
  */
2024
2024
  declare const loadAnalyticsRuntimeMetrics: (source: AnalyticsMetricsSource, options: AnalyticsMetricsOptions) => Promise<AnalyticsRuntimeMetrics>;
2025
2025
  /**
2026
+ * Disambiguate {@link Finding}s that share a `cacheKey`.
2027
+ *
2028
+ * A `cacheKey` is the studio's dedup / dismissal id (see {@link Finding.cacheKey}):
2029
+ * two findings with the same key collapse to one row, and a single dismissal
2030
+ * silences both. A lint keyed on `name:file:line` (the argument-derived sinks,
2031
+ * `sql_injection_risk`, `kv_unscoped_user_key_idor`, `hardcoded_secret`, …) can
2032
+ * legitimately emit two occurrences on one physical source line, so without a
2033
+ * within-line discriminator the second finding is silently hidden.
2034
+ *
2035
+ * This suffixes the second-and-later occurrence of any repeated key with
2036
+ * `:&lt;n>` (`:2`, `:3`, …), leaving the first occurrence unsuffixed so existing
2037
+ * single-occurrence keys stay stable across runs. Order is preserved. Keys are
2038
+ * lint-name-prefixed, so this never merges across lints.
2039
+ */
2040
+ declare const dedupeCacheKeys: (findings: ReadonlyArray<Finding>) => Finding[];
2041
+ /**
2026
2042
  * Constraint validator — flag rows that violate declared FK / NOT NULL / UNIQUE
2027
2043
  * constraints by cross-checking sampled row data against the schema.
2028
2044
  *
@@ -2351,7 +2367,7 @@ declare const browserAllowPrivateTargets: Lint;
2351
2367
  */
2352
2368
  declare const browserUserUrlWithoutAllowlist: Lint;
2353
2369
  /**
2354
- * Detect FK cycles in the declared relation graph via a DFS.
2370
+ * Detect FK cycles in the declared relation graph.
2355
2371
  *
2356
2372
  * A "circular FK" exists when a chain of `one` relations forms a loop — for
2357
2373
  * example `A.authorId → B`, `B.ownerId → C`, `C.postId → A`. Such cycles can
@@ -2370,11 +2386,18 @@ declare const browserUserUrlWithoutAllowlist: Lint;
2370
2386
  * would be noise. Only multi-table cycles — the ones the description illustrates
2371
2387
  * — are surfaced.
2372
2388
  *
2373
- * Each unique cycle is reported once: the cycle path is canonicalized to its
2374
- * lexicographically smallest rotation so two DFS traversals that enter the same
2375
- * ring at different nodes emit the same cacheKey and detail. A representative
2376
- * cycle is reported for each distinct simple cycle in the graph; overlapping or
2377
- * chord cycles that share interior nodes are each detected independently.
2389
+ * Cycle enumeration uses **Johnson's algorithm** rather than a naive
2390
+ * enumerate-all-paths DFS. A plain path-DFS re-walks every simple path in the
2391
+ * graph, which is worst-case exponential even on a fully **acyclic** schema
2392
+ * (reconverging FK fan-out many tables referencing shared parents in a chain
2393
+ * makes codegen appear to hang). Johnson blocks a vertex once a fruitless
2394
+ * subtree is exhausted and only unblocks it when a new cycle through it is
2395
+ * found, so it runs in `O((V + E)(C + 1))` for `C` elementary circuits — no
2396
+ * blowup on acyclic input. Each circuit is enumerated exactly once from its
2397
+ * lowest-indexed member (vertices are ordered lexicographically), so overlapping
2398
+ * / chord cycles that share interior nodes are each detected independently. The
2399
+ * emitted cycle is still canonicalized to its lexicographically smallest
2400
+ * rotation for a stable cacheKey.
2378
2401
  */
2379
2402
  declare const circularFk: Lint;
2380
2403
  /**
@@ -3604,4 +3627,4 @@ interface RunAdvisorOptions {
3604
3627
  * `static` lints at build time and defer `runtime` lints to a live shard.
3605
3628
  */
3606
3629
  declare const runAdvisor: (context: LintContext, options?: RunAdvisorOptions) => Finding[];
3607
- export { AE_METRIC_EVENTS, ALL_LINTS, type AdvisorAdminRoute, type AdvisorAiRawRun, type AdvisorAiToolSideEffect, type AdvisorArgumentDerivedFetch, type AdvisorArgumentValidator, type AdvisorAuthApiCall, type AdvisorAuthConfig, type AdvisorBrowserUrlAccess, type AdvisorConfigCall, type AdvisorContainer, type AdvisorContainerKeyAccess, type AdvisorContainerOverride, type AdvisorFailOpenGuard, type AdvisorFlagSecurityDefault, type AdvisorHttpActionGuard, type AdvisorHttpHeaderWrite, type AdvisorHyperdriveCall, type AdvisorIdentityClaimRead, type AdvisorImageDeliveryUrlAccess, type AdvisorIndex, type AdvisorIndexHit, type AdvisorInsertWrite, type AdvisorKvKeyAccess, type AdvisorMailRecipientAccess, type AdvisorMaskProcedure, type AdvisorMaskStrategy, type AdvisorMutatorWrite, type AdvisorNondeterministicCall, type AdvisorNormalizeIdAuthorization, type AdvisorOwnerFieldWrite, type AdvisorPaymentWebhook, type AdvisorPrivilegedDispatch, type AdvisorProcedureProtection, type AdvisorQueryRead, type AdvisorQueue, type AdvisorQueueTuning, type AdvisorR2sqlCall, type AdvisorRatelimitKeySelector, type AdvisorRawRowReturn, type AdvisorRelation, type AdvisorRelationLoad, type AdvisorRlsProcedure, type AdvisorSchema, type AdvisorSecretLiteral, type AdvisorShape, type AdvisorShardTraffic, type AdvisorSoftDeleteRead, type AdvisorSqlInterpolation, type AdvisorStorageKeyAccess, type AdvisorStorageUpload, type AdvisorTable, type AdvisorTableSample, type AdvisorTableScan, type AdvisorVectorNamespaceAccess, type AdvisorWorkflow, type AdvisorWorkflowCall, type AdvisorWranglerVariable, type AnalyticsMetricsOptions, type AnalyticsMetricsSource, type AnalyticsRuntimeMetrics, type Category, type Facing, type Finding, type Level, type Lint, type LintContext, type LintSource, RUNTIME_LINTS, RunAdvisorOptions, STATIC_LINTS, actionFetchSsrf, adminRouteWithoutGuard, aiRawRunEscapeHatch, aiToolSideEffectPromptInjection, aiUnboundedGenerationPublic, allowUnauthenticatedShardAccessEnabled, authApiCallWithoutHeaders, authCsrfCheckDisabled, authEmailVerificationDisabled, authSecureCookiesDisabled, authSessionFreshageZero, authTrustedOriginsWildcard, browserAllowPrivateTargets, browserUserUrlWithoutAllowlist, circularFk, constraintValidator, containerInstanceKeyFromUserInput, containerOversizedInstance, containerPublicInternet, containerRuntimeEgressRelaxation, containerStartEnableInternetOverride, duplicateIndex, emptyIndex, externalSourceOnGlobal, externalSourceUnscoped, filterWithoutIndex, flagGatesSecurityWithUnsafeDefault, fromServerSchema, hardcodedSecret, hotShard, httpActionMissingAuthGuard, httpActionResponseHeaderInjection, hyperdriveOutsideAction, identityUndeclaredClaimTrusted, imagesUrlSourceFromUserInput, indexReferencesUnknownField, indexUtilization, insertManyUnsafeUserData, kvUnscopedUserKeyIdor, loadAnalyticsRuntimeMetrics, mailInboundDispatchWithoutVerify, mailRecipientFromRequestInput, maskUncoveredPiiColumn, maskWeakHashStrategyOnPii, maskedRelationLeakViaWith, mutatorFullRowReplace, nondeterministicQueryMutation, normalizeIdUsedAsAuthorization, outputProjectionMissingOnPublicRead, ownerFieldFromArgsNotAuth, paymentCreateWithoutAuthorize, paymentWebhookWideTolerance, plaintextSecretInWranglerVariables, policyReferencesUnknownTable, privilegedDispatchUnvalidatedPayload, privilegedFanoutFromPublicProcedure, publicArgumentUsesAny, publicMutationWithoutRatelimit, publicTableRlsOptoutConfusion, queueWithoutDlq, r2sqlOutsideAction, ratelimitDefaultMemoryStore, ratelimitKeySpoofableOrGlobal, ratelimitMiddlewareFailOpen, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, shapeTargetsGlobalTable, shapeUnknownTable, softDeleteIncludeDeletedFromArgs, sqlInjectionRisk, storageGenerateUploadUrlNoContentTypePin, storageKeyFromUserArgs, storagePresignedUrlForPrivateContent, storageUploadWithoutContentTypeAllowlist, storageUploadWithoutMaxSize, tableWithoutInsert, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, userCreatingMutationWithoutCaptcha, vectorsNamespaceFromUserInput, workflowDuplicateStepName, workflowUnknownTarget, workflowUnused };
3630
+ export { AE_METRIC_EVENTS, ALL_LINTS, type AdvisorAdminRoute, type AdvisorAiRawRun, type AdvisorAiToolSideEffect, type AdvisorArgumentDerivedFetch, type AdvisorArgumentValidator, type AdvisorAuthApiCall, type AdvisorAuthConfig, type AdvisorBrowserUrlAccess, type AdvisorConfigCall, type AdvisorContainer, type AdvisorContainerKeyAccess, type AdvisorContainerOverride, type AdvisorFailOpenGuard, type AdvisorFlagSecurityDefault, type AdvisorHttpActionGuard, type AdvisorHttpHeaderWrite, type AdvisorHyperdriveCall, type AdvisorIdentityClaimRead, type AdvisorImageDeliveryUrlAccess, type AdvisorIndex, type AdvisorIndexHit, type AdvisorInsertWrite, type AdvisorKvKeyAccess, type AdvisorMailRecipientAccess, type AdvisorMaskProcedure, type AdvisorMaskStrategy, type AdvisorMutatorWrite, type AdvisorNondeterministicCall, type AdvisorNormalizeIdAuthorization, type AdvisorOwnerFieldWrite, type AdvisorPaymentWebhook, type AdvisorPrivilegedDispatch, type AdvisorProcedureProtection, type AdvisorQueryRead, type AdvisorQueue, type AdvisorQueueTuning, type AdvisorR2sqlCall, type AdvisorRatelimitKeySelector, type AdvisorRawRowReturn, type AdvisorRelation, type AdvisorRelationLoad, type AdvisorRlsProcedure, type AdvisorSchema, type AdvisorSecretLiteral, type AdvisorShape, type AdvisorShardTraffic, type AdvisorSoftDeleteRead, type AdvisorSqlInterpolation, type AdvisorStorageKeyAccess, type AdvisorStorageUpload, type AdvisorTable, type AdvisorTableSample, type AdvisorTableScan, type AdvisorVectorNamespaceAccess, type AdvisorWorkflow, type AdvisorWorkflowCall, type AdvisorWranglerVariable, type AnalyticsMetricsOptions, type AnalyticsMetricsSource, type AnalyticsRuntimeMetrics, type Category, type Facing, type Finding, type Level, type Lint, type LintContext, type LintSource, RUNTIME_LINTS, RunAdvisorOptions, STATIC_LINTS, actionFetchSsrf, adminRouteWithoutGuard, aiRawRunEscapeHatch, aiToolSideEffectPromptInjection, aiUnboundedGenerationPublic, allowUnauthenticatedShardAccessEnabled, authApiCallWithoutHeaders, authCsrfCheckDisabled, authEmailVerificationDisabled, authSecureCookiesDisabled, authSessionFreshageZero, authTrustedOriginsWildcard, browserAllowPrivateTargets, browserUserUrlWithoutAllowlist, circularFk, constraintValidator, containerInstanceKeyFromUserInput, containerOversizedInstance, containerPublicInternet, containerRuntimeEgressRelaxation, containerStartEnableInternetOverride, dedupeCacheKeys, duplicateIndex, emptyIndex, externalSourceOnGlobal, externalSourceUnscoped, filterWithoutIndex, flagGatesSecurityWithUnsafeDefault, fromServerSchema, hardcodedSecret, hotShard, httpActionMissingAuthGuard, httpActionResponseHeaderInjection, hyperdriveOutsideAction, identityUndeclaredClaimTrusted, imagesUrlSourceFromUserInput, indexReferencesUnknownField, indexUtilization, insertManyUnsafeUserData, kvUnscopedUserKeyIdor, loadAnalyticsRuntimeMetrics, mailInboundDispatchWithoutVerify, mailRecipientFromRequestInput, maskUncoveredPiiColumn, maskWeakHashStrategyOnPii, maskedRelationLeakViaWith, mutatorFullRowReplace, nondeterministicQueryMutation, normalizeIdUsedAsAuthorization, outputProjectionMissingOnPublicRead, ownerFieldFromArgsNotAuth, paymentCreateWithoutAuthorize, paymentWebhookWideTolerance, plaintextSecretInWranglerVariables, policyReferencesUnknownTable, privilegedDispatchUnvalidatedPayload, privilegedFanoutFromPublicProcedure, publicArgumentUsesAny, publicMutationWithoutRatelimit, publicTableRlsOptoutConfusion, queueWithoutDlq, r2sqlOutsideAction, ratelimitDefaultMemoryStore, ratelimitKeySpoofableOrGlobal, ratelimitMiddlewareFailOpen, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, shapeTargetsGlobalTable, shapeUnknownTable, softDeleteIncludeDeletedFromArgs, sqlInjectionRisk, storageGenerateUploadUrlNoContentTypePin, storageKeyFromUserArgs, storagePresignedUrlForPrivateContent, storageUploadWithoutContentTypeAllowlist, storageUploadWithoutMaxSize, tableWithoutInsert, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, userCreatingMutationWithoutCaptcha, vectorsNamespaceFromUserInput, workflowDuplicateStepName, workflowUnknownTarget, workflowUnused };
package/dist/index.d.ts CHANGED
@@ -2023,6 +2023,22 @@ interface AnalyticsRuntimeMetrics {
2023
2023
  */
2024
2024
  declare const loadAnalyticsRuntimeMetrics: (source: AnalyticsMetricsSource, options: AnalyticsMetricsOptions) => Promise<AnalyticsRuntimeMetrics>;
2025
2025
  /**
2026
+ * Disambiguate {@link Finding}s that share a `cacheKey`.
2027
+ *
2028
+ * A `cacheKey` is the studio's dedup / dismissal id (see {@link Finding.cacheKey}):
2029
+ * two findings with the same key collapse to one row, and a single dismissal
2030
+ * silences both. A lint keyed on `name:file:line` (the argument-derived sinks,
2031
+ * `sql_injection_risk`, `kv_unscoped_user_key_idor`, `hardcoded_secret`, …) can
2032
+ * legitimately emit two occurrences on one physical source line, so without a
2033
+ * within-line discriminator the second finding is silently hidden.
2034
+ *
2035
+ * This suffixes the second-and-later occurrence of any repeated key with
2036
+ * `:&lt;n>` (`:2`, `:3`, …), leaving the first occurrence unsuffixed so existing
2037
+ * single-occurrence keys stay stable across runs. Order is preserved. Keys are
2038
+ * lint-name-prefixed, so this never merges across lints.
2039
+ */
2040
+ declare const dedupeCacheKeys: (findings: ReadonlyArray<Finding>) => Finding[];
2041
+ /**
2026
2042
  * Constraint validator — flag rows that violate declared FK / NOT NULL / UNIQUE
2027
2043
  * constraints by cross-checking sampled row data against the schema.
2028
2044
  *
@@ -2351,7 +2367,7 @@ declare const browserAllowPrivateTargets: Lint;
2351
2367
  */
2352
2368
  declare const browserUserUrlWithoutAllowlist: Lint;
2353
2369
  /**
2354
- * Detect FK cycles in the declared relation graph via a DFS.
2370
+ * Detect FK cycles in the declared relation graph.
2355
2371
  *
2356
2372
  * A "circular FK" exists when a chain of `one` relations forms a loop — for
2357
2373
  * example `A.authorId → B`, `B.ownerId → C`, `C.postId → A`. Such cycles can
@@ -2370,11 +2386,18 @@ declare const browserUserUrlWithoutAllowlist: Lint;
2370
2386
  * would be noise. Only multi-table cycles — the ones the description illustrates
2371
2387
  * — are surfaced.
2372
2388
  *
2373
- * Each unique cycle is reported once: the cycle path is canonicalized to its
2374
- * lexicographically smallest rotation so two DFS traversals that enter the same
2375
- * ring at different nodes emit the same cacheKey and detail. A representative
2376
- * cycle is reported for each distinct simple cycle in the graph; overlapping or
2377
- * chord cycles that share interior nodes are each detected independently.
2389
+ * Cycle enumeration uses **Johnson's algorithm** rather than a naive
2390
+ * enumerate-all-paths DFS. A plain path-DFS re-walks every simple path in the
2391
+ * graph, which is worst-case exponential even on a fully **acyclic** schema
2392
+ * (reconverging FK fan-out many tables referencing shared parents in a chain
2393
+ * makes codegen appear to hang). Johnson blocks a vertex once a fruitless
2394
+ * subtree is exhausted and only unblocks it when a new cycle through it is
2395
+ * found, so it runs in `O((V + E)(C + 1))` for `C` elementary circuits — no
2396
+ * blowup on acyclic input. Each circuit is enumerated exactly once from its
2397
+ * lowest-indexed member (vertices are ordered lexicographically), so overlapping
2398
+ * / chord cycles that share interior nodes are each detected independently. The
2399
+ * emitted cycle is still canonicalized to its lexicographically smallest
2400
+ * rotation for a stable cacheKey.
2378
2401
  */
2379
2402
  declare const circularFk: Lint;
2380
2403
  /**
@@ -3604,4 +3627,4 @@ interface RunAdvisorOptions {
3604
3627
  * `static` lints at build time and defer `runtime` lints to a live shard.
3605
3628
  */
3606
3629
  declare const runAdvisor: (context: LintContext, options?: RunAdvisorOptions) => Finding[];
3607
- export { AE_METRIC_EVENTS, ALL_LINTS, type AdvisorAdminRoute, type AdvisorAiRawRun, type AdvisorAiToolSideEffect, type AdvisorArgumentDerivedFetch, type AdvisorArgumentValidator, type AdvisorAuthApiCall, type AdvisorAuthConfig, type AdvisorBrowserUrlAccess, type AdvisorConfigCall, type AdvisorContainer, type AdvisorContainerKeyAccess, type AdvisorContainerOverride, type AdvisorFailOpenGuard, type AdvisorFlagSecurityDefault, type AdvisorHttpActionGuard, type AdvisorHttpHeaderWrite, type AdvisorHyperdriveCall, type AdvisorIdentityClaimRead, type AdvisorImageDeliveryUrlAccess, type AdvisorIndex, type AdvisorIndexHit, type AdvisorInsertWrite, type AdvisorKvKeyAccess, type AdvisorMailRecipientAccess, type AdvisorMaskProcedure, type AdvisorMaskStrategy, type AdvisorMutatorWrite, type AdvisorNondeterministicCall, type AdvisorNormalizeIdAuthorization, type AdvisorOwnerFieldWrite, type AdvisorPaymentWebhook, type AdvisorPrivilegedDispatch, type AdvisorProcedureProtection, type AdvisorQueryRead, type AdvisorQueue, type AdvisorQueueTuning, type AdvisorR2sqlCall, type AdvisorRatelimitKeySelector, type AdvisorRawRowReturn, type AdvisorRelation, type AdvisorRelationLoad, type AdvisorRlsProcedure, type AdvisorSchema, type AdvisorSecretLiteral, type AdvisorShape, type AdvisorShardTraffic, type AdvisorSoftDeleteRead, type AdvisorSqlInterpolation, type AdvisorStorageKeyAccess, type AdvisorStorageUpload, type AdvisorTable, type AdvisorTableSample, type AdvisorTableScan, type AdvisorVectorNamespaceAccess, type AdvisorWorkflow, type AdvisorWorkflowCall, type AdvisorWranglerVariable, type AnalyticsMetricsOptions, type AnalyticsMetricsSource, type AnalyticsRuntimeMetrics, type Category, type Facing, type Finding, type Level, type Lint, type LintContext, type LintSource, RUNTIME_LINTS, RunAdvisorOptions, STATIC_LINTS, actionFetchSsrf, adminRouteWithoutGuard, aiRawRunEscapeHatch, aiToolSideEffectPromptInjection, aiUnboundedGenerationPublic, allowUnauthenticatedShardAccessEnabled, authApiCallWithoutHeaders, authCsrfCheckDisabled, authEmailVerificationDisabled, authSecureCookiesDisabled, authSessionFreshageZero, authTrustedOriginsWildcard, browserAllowPrivateTargets, browserUserUrlWithoutAllowlist, circularFk, constraintValidator, containerInstanceKeyFromUserInput, containerOversizedInstance, containerPublicInternet, containerRuntimeEgressRelaxation, containerStartEnableInternetOverride, duplicateIndex, emptyIndex, externalSourceOnGlobal, externalSourceUnscoped, filterWithoutIndex, flagGatesSecurityWithUnsafeDefault, fromServerSchema, hardcodedSecret, hotShard, httpActionMissingAuthGuard, httpActionResponseHeaderInjection, hyperdriveOutsideAction, identityUndeclaredClaimTrusted, imagesUrlSourceFromUserInput, indexReferencesUnknownField, indexUtilization, insertManyUnsafeUserData, kvUnscopedUserKeyIdor, loadAnalyticsRuntimeMetrics, mailInboundDispatchWithoutVerify, mailRecipientFromRequestInput, maskUncoveredPiiColumn, maskWeakHashStrategyOnPii, maskedRelationLeakViaWith, mutatorFullRowReplace, nondeterministicQueryMutation, normalizeIdUsedAsAuthorization, outputProjectionMissingOnPublicRead, ownerFieldFromArgsNotAuth, paymentCreateWithoutAuthorize, paymentWebhookWideTolerance, plaintextSecretInWranglerVariables, policyReferencesUnknownTable, privilegedDispatchUnvalidatedPayload, privilegedFanoutFromPublicProcedure, publicArgumentUsesAny, publicMutationWithoutRatelimit, publicTableRlsOptoutConfusion, queueWithoutDlq, r2sqlOutsideAction, ratelimitDefaultMemoryStore, ratelimitKeySpoofableOrGlobal, ratelimitMiddlewareFailOpen, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, shapeTargetsGlobalTable, shapeUnknownTable, softDeleteIncludeDeletedFromArgs, sqlInjectionRisk, storageGenerateUploadUrlNoContentTypePin, storageKeyFromUserArgs, storagePresignedUrlForPrivateContent, storageUploadWithoutContentTypeAllowlist, storageUploadWithoutMaxSize, tableWithoutInsert, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, userCreatingMutationWithoutCaptcha, vectorsNamespaceFromUserInput, workflowDuplicateStepName, workflowUnknownTarget, workflowUnused };
3630
+ export { AE_METRIC_EVENTS, ALL_LINTS, type AdvisorAdminRoute, type AdvisorAiRawRun, type AdvisorAiToolSideEffect, type AdvisorArgumentDerivedFetch, type AdvisorArgumentValidator, type AdvisorAuthApiCall, type AdvisorAuthConfig, type AdvisorBrowserUrlAccess, type AdvisorConfigCall, type AdvisorContainer, type AdvisorContainerKeyAccess, type AdvisorContainerOverride, type AdvisorFailOpenGuard, type AdvisorFlagSecurityDefault, type AdvisorHttpActionGuard, type AdvisorHttpHeaderWrite, type AdvisorHyperdriveCall, type AdvisorIdentityClaimRead, type AdvisorImageDeliveryUrlAccess, type AdvisorIndex, type AdvisorIndexHit, type AdvisorInsertWrite, type AdvisorKvKeyAccess, type AdvisorMailRecipientAccess, type AdvisorMaskProcedure, type AdvisorMaskStrategy, type AdvisorMutatorWrite, type AdvisorNondeterministicCall, type AdvisorNormalizeIdAuthorization, type AdvisorOwnerFieldWrite, type AdvisorPaymentWebhook, type AdvisorPrivilegedDispatch, type AdvisorProcedureProtection, type AdvisorQueryRead, type AdvisorQueue, type AdvisorQueueTuning, type AdvisorR2sqlCall, type AdvisorRatelimitKeySelector, type AdvisorRawRowReturn, type AdvisorRelation, type AdvisorRelationLoad, type AdvisorRlsProcedure, type AdvisorSchema, type AdvisorSecretLiteral, type AdvisorShape, type AdvisorShardTraffic, type AdvisorSoftDeleteRead, type AdvisorSqlInterpolation, type AdvisorStorageKeyAccess, type AdvisorStorageUpload, type AdvisorTable, type AdvisorTableSample, type AdvisorTableScan, type AdvisorVectorNamespaceAccess, type AdvisorWorkflow, type AdvisorWorkflowCall, type AdvisorWranglerVariable, type AnalyticsMetricsOptions, type AnalyticsMetricsSource, type AnalyticsRuntimeMetrics, type Category, type Facing, type Finding, type Level, type Lint, type LintContext, type LintSource, RUNTIME_LINTS, RunAdvisorOptions, STATIC_LINTS, actionFetchSsrf, adminRouteWithoutGuard, aiRawRunEscapeHatch, aiToolSideEffectPromptInjection, aiUnboundedGenerationPublic, allowUnauthenticatedShardAccessEnabled, authApiCallWithoutHeaders, authCsrfCheckDisabled, authEmailVerificationDisabled, authSecureCookiesDisabled, authSessionFreshageZero, authTrustedOriginsWildcard, browserAllowPrivateTargets, browserUserUrlWithoutAllowlist, circularFk, constraintValidator, containerInstanceKeyFromUserInput, containerOversizedInstance, containerPublicInternet, containerRuntimeEgressRelaxation, containerStartEnableInternetOverride, dedupeCacheKeys, duplicateIndex, emptyIndex, externalSourceOnGlobal, externalSourceUnscoped, filterWithoutIndex, flagGatesSecurityWithUnsafeDefault, fromServerSchema, hardcodedSecret, hotShard, httpActionMissingAuthGuard, httpActionResponseHeaderInjection, hyperdriveOutsideAction, identityUndeclaredClaimTrusted, imagesUrlSourceFromUserInput, indexReferencesUnknownField, indexUtilization, insertManyUnsafeUserData, kvUnscopedUserKeyIdor, loadAnalyticsRuntimeMetrics, mailInboundDispatchWithoutVerify, mailRecipientFromRequestInput, maskUncoveredPiiColumn, maskWeakHashStrategyOnPii, maskedRelationLeakViaWith, mutatorFullRowReplace, nondeterministicQueryMutation, normalizeIdUsedAsAuthorization, outputProjectionMissingOnPublicRead, ownerFieldFromArgsNotAuth, paymentCreateWithoutAuthorize, paymentWebhookWideTolerance, plaintextSecretInWranglerVariables, policyReferencesUnknownTable, privilegedDispatchUnvalidatedPayload, privilegedFanoutFromPublicProcedure, publicArgumentUsesAny, publicMutationWithoutRatelimit, publicTableRlsOptoutConfusion, queueWithoutDlq, r2sqlOutsideAction, ratelimitDefaultMemoryStore, ratelimitKeySpoofableOrGlobal, ratelimitMiddlewareFailOpen, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, shapeTargetsGlobalTable, shapeUnknownTable, softDeleteIncludeDeletedFromArgs, sqlInjectionRisk, storageGenerateUploadUrlNoContentTypePin, storageKeyFromUserArgs, storagePresignedUrlForPrivateContent, storageUploadWithoutContentTypeAllowlist, storageUploadWithoutMaxSize, tableWithoutInsert, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, userCreatingMutationWithoutCaptcha, vectorsNamespaceFromUserInput, workflowDuplicateStepName, workflowUnknownTarget, workflowUnused };
package/dist/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
- import constraintValidator from './packem_shared/constraintValidator-Dr9Py3FD.mjs';
2
- import hotShard from './packem_shared/hotShard-Ir5D0B6J.mjs';
1
+ import { dedupeCacheKeys } from './packem_shared/dedupeCacheKeys-r5B7u_yq.mjs';
2
+ import constraintValidator from './packem_shared/constraintValidator-CxwtpJ6E.mjs';
3
+ import hotShard from './packem_shared/hotShard-CkC7qpre.mjs';
3
4
  import indexUtilization from './packem_shared/indexUtilization-B5DMQ3bI.mjs';
4
5
  import actionFetchSsrf from './packem_shared/actionFetchSsrf-wbmYgzmz.mjs';
5
6
  import adminRouteWithoutGuard from './packem_shared/adminRouteWithoutGuard-UUGBkAjU.mjs';
@@ -15,7 +16,7 @@ import authSessionFreshageZero from './packem_shared/authSessionFreshageZero-Cju
15
16
  import authTrustedOriginsWildcard from './packem_shared/authTrustedOriginsWildcard-ylv4PKzC.mjs';
16
17
  import browserAllowPrivateTargets from './packem_shared/browserAllowPrivateTargets-CBvQxLAM.mjs';
17
18
  import browserUserUrlWithoutAllowlist from './packem_shared/browserUserUrlWithoutAllowlist-CvhA6w59.mjs';
18
- import circularFk from './packem_shared/circularFk-B2freHrP.mjs';
19
+ import circularFk from './packem_shared/circularFk-CNcAVuYa.mjs';
19
20
  import containerInstanceKeyFromUserInput from './packem_shared/containerInstanceKeyFromUserInput-BUXj2J4w.mjs';
20
21
  import containerOversizedInstance from './packem_shared/containerOversizedInstance-5U1VKPRM.mjs';
21
22
  import containerPublicInternet from './packem_shared/containerPublicInternet-CuNerJE5.mjs';
@@ -23,9 +24,11 @@ import containerRuntimeEgressRelaxation from './packem_shared/containerRuntimeEg
23
24
  import containerStartEnableInternetOverride from './packem_shared/containerStartEnableInternetOverride-DDaHZQ1L.mjs';
24
25
  import duplicateIndex from './packem_shared/duplicateIndex-BOublMSt.mjs';
25
26
  import emptyIndex from './packem_shared/emptyIndex-BX8EuEY7.mjs';
27
+ import externalSourceOnGlobal from './packem_shared/externalSourceOnGlobal-Bg-NfCX9.mjs';
28
+ import externalSourceUnscoped from './packem_shared/externalSourceUnscoped-5vT-Bup3.mjs';
26
29
  import filterWithoutIndex from './packem_shared/filterWithoutIndex-BYVeJaSs.mjs';
27
30
  import flagGatesSecurityWithUnsafeDefault from './packem_shared/flagGatesSecurityWithUnsafeDefault-BhIs0shr.mjs';
28
- import hardcodedSecret from './packem_shared/hardcodedSecret-W2pz1UZB.mjs';
31
+ import hardcodedSecret from './packem_shared/hardcodedSecret-Be-pKVdn.mjs';
29
32
  import httpActionMissingAuthGuard from './packem_shared/httpActionMissingAuthGuard-CxipddNx.mjs';
30
33
  import httpActionResponseHeaderInjection from './packem_shared/httpActionResponseHeaderInjection-DOFS7pFT.mjs';
31
34
  import hyperdriveOutsideAction from './packem_shared/hyperdriveOutsideAction-BgZqX7Xg.mjs';
@@ -80,8 +83,6 @@ import workflowDuplicateStepName from './packem_shared/workflowDuplicateStepName
80
83
  import workflowUnknownTarget from './packem_shared/workflowUnknownTarget-Cdd7WhKQ.mjs';
81
84
  import workflowUnused from './packem_shared/workflowUnused-D0jHxdz9.mjs';
82
85
  export { AE_METRIC_EVENTS, loadAnalyticsRuntimeMetrics } from './packem_shared/AE_METRIC_EVENTS-BM14d0lm.mjs';
83
- export { default as externalSourceOnGlobal } from './packem_shared/externalSourceOnGlobal-Bg-NfCX9.mjs';
84
- export { default as externalSourceUnscoped } from './packem_shared/externalSourceUnscoped-5vT-Bup3.mjs';
85
86
  export { fromServerSchema } from './packem_shared/fromServerSchema-BiB6JSiW.mjs';
86
87
 
87
88
  const STATIC_LINTS = [
@@ -91,6 +92,8 @@ const STATIC_LINTS = [
91
92
  workflowUnknownTarget,
92
93
  workflowDuplicateStepName,
93
94
  shapeUnknownTable,
95
+ externalSourceOnGlobal,
96
+ externalSourceUnscoped,
94
97
  emptyIndex,
95
98
  circularFk,
96
99
  unindexedForeignKey,
@@ -175,7 +178,7 @@ const runAdvisor = (context, options = {}) => {
175
178
  }
176
179
  findings.push(...lint.run(context));
177
180
  }
178
- return findings;
181
+ return dedupeCacheKeys(findings);
179
182
  };
180
183
 
181
- export { ALL_LINTS, RUNTIME_LINTS, STATIC_LINTS, actionFetchSsrf, adminRouteWithoutGuard, aiRawRunEscapeHatch, aiToolSideEffectPromptInjection, aiUnboundedGenerationPublic, allowUnauthenticatedShardAccessEnabled, authApiCallWithoutHeaders, authCsrfCheckDisabled, authEmailVerificationDisabled, authSecureCookiesDisabled, authSessionFreshageZero, authTrustedOriginsWildcard, browserAllowPrivateTargets, browserUserUrlWithoutAllowlist, circularFk, constraintValidator, containerInstanceKeyFromUserInput, containerOversizedInstance, containerPublicInternet, containerRuntimeEgressRelaxation, containerStartEnableInternetOverride, duplicateIndex, emptyIndex, filterWithoutIndex, flagGatesSecurityWithUnsafeDefault, hardcodedSecret, hotShard, httpActionMissingAuthGuard, httpActionResponseHeaderInjection, hyperdriveOutsideAction, identityUndeclaredClaimTrusted, imagesUrlSourceFromUserInput, indexReferencesUnknownField, indexUtilization, insertManyUnsafeUserData, kvUnscopedUserKeyIdor, mailInboundDispatchWithoutVerify, mailRecipientFromRequestInput, maskUncoveredPiiColumn, maskWeakHashStrategyOnPii, maskedRelationLeakViaWith, mutatorFullRowReplace, nondeterministicQueryMutation, normalizeIdUsedAsAuthorization, outputProjectionMissingOnPublicRead, ownerFieldFromArgsNotAuth, paymentCreateWithoutAuthorize, paymentWebhookWideTolerance, plaintextSecretInWranglerVariables, policyReferencesUnknownTable, privilegedDispatchUnvalidatedPayload, privilegedFanoutFromPublicProcedure, publicArgumentUsesAny, publicMutationWithoutRatelimit, publicTableRlsOptoutConfusion, queueWithoutDlq, r2sqlOutsideAction, ratelimitDefaultMemoryStore, ratelimitKeySpoofableOrGlobal, ratelimitMiddlewareFailOpen, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, shapeTargetsGlobalTable, shapeUnknownTable, softDeleteIncludeDeletedFromArgs, sqlInjectionRisk, storageGenerateUploadUrlNoContentTypePin, storageKeyFromUserArgs, storagePresignedUrlForPrivateContent, storageUploadWithoutContentTypeAllowlist, storageUploadWithoutMaxSize, tableWithoutInsert, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, userCreatingMutationWithoutCaptcha, vectorsNamespaceFromUserInput, workflowDuplicateStepName, workflowUnknownTarget, workflowUnused };
184
+ export { ALL_LINTS, RUNTIME_LINTS, STATIC_LINTS, actionFetchSsrf, adminRouteWithoutGuard, aiRawRunEscapeHatch, aiToolSideEffectPromptInjection, aiUnboundedGenerationPublic, allowUnauthenticatedShardAccessEnabled, authApiCallWithoutHeaders, authCsrfCheckDisabled, authEmailVerificationDisabled, authSecureCookiesDisabled, authSessionFreshageZero, authTrustedOriginsWildcard, browserAllowPrivateTargets, browserUserUrlWithoutAllowlist, circularFk, constraintValidator, containerInstanceKeyFromUserInput, containerOversizedInstance, containerPublicInternet, containerRuntimeEgressRelaxation, containerStartEnableInternetOverride, dedupeCacheKeys, duplicateIndex, emptyIndex, externalSourceOnGlobal, externalSourceUnscoped, filterWithoutIndex, flagGatesSecurityWithUnsafeDefault, hardcodedSecret, hotShard, httpActionMissingAuthGuard, httpActionResponseHeaderInjection, hyperdriveOutsideAction, identityUndeclaredClaimTrusted, imagesUrlSourceFromUserInput, indexReferencesUnknownField, indexUtilization, insertManyUnsafeUserData, kvUnscopedUserKeyIdor, mailInboundDispatchWithoutVerify, mailRecipientFromRequestInput, maskUncoveredPiiColumn, maskWeakHashStrategyOnPii, maskedRelationLeakViaWith, mutatorFullRowReplace, nondeterministicQueryMutation, normalizeIdUsedAsAuthorization, outputProjectionMissingOnPublicRead, ownerFieldFromArgsNotAuth, paymentCreateWithoutAuthorize, paymentWebhookWideTolerance, plaintextSecretInWranglerVariables, policyReferencesUnknownTable, privilegedDispatchUnvalidatedPayload, privilegedFanoutFromPublicProcedure, publicArgumentUsesAny, publicMutationWithoutRatelimit, publicTableRlsOptoutConfusion, queueWithoutDlq, r2sqlOutsideAction, ratelimitDefaultMemoryStore, ratelimitKeySpoofableOrGlobal, ratelimitMiddlewareFailOpen, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, shapeTargetsGlobalTable, shapeUnknownTable, softDeleteIncludeDeletedFromArgs, sqlInjectionRisk, storageGenerateUploadUrlNoContentTypePin, storageKeyFromUserArgs, storagePresignedUrlForPrivateContent, storageUploadWithoutContentTypeAllowlist, storageUploadWithoutMaxSize, tableWithoutInsert, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, userCreatingMutationWithoutCaptcha, vectorsNamespaceFromUserInput, workflowDuplicateStepName, workflowUnknownTarget, workflowUnused };
@@ -0,0 +1,127 @@
1
+ import { e as emit } from './finding-Dm_zvzS1.mjs';
2
+
3
+ const MAX_CYCLES = 100;
4
+ const circularFk = {
5
+ categories: ["SCHEMA"],
6
+ description: "A chain of foreign-key relations forms a cycle (e.g. A → B → C → A). Circular FK dependencies can cause unexpected behavior during DELETE operations — a CASCADE chain may loop indefinitely, and even a RESTRICT cycle prevents deletion of any row in the loop without temporarily disabling constraints.",
7
+ facing: "INTERNAL",
8
+ level: "WARN",
9
+ name: "circular_fk",
10
+ remediation: "Remove or break the cycle by dropping at least one FK relation from the loop. Consider replacing the circular dependency with a nullable FK and explicit application logic, or restructure the schema using a junction table.",
11
+ run: (context) => {
12
+ const findings = [];
13
+ const order = [...new Set(context.schema.tables.map((table) => table.name))].sort();
14
+ const indexOf = new Map(order.map((name, index) => [name, index]));
15
+ const adjacency = /* @__PURE__ */ new Map();
16
+ for (const table of context.schema.tables) {
17
+ for (const relation of table.relations) {
18
+ if (relation.kind !== "one" || !indexOf.has(relation.table)) {
19
+ continue;
20
+ }
21
+ let targets = adjacency.get(table.name);
22
+ if (targets === void 0) {
23
+ targets = /* @__PURE__ */ new Set();
24
+ adjacency.set(table.name, targets);
25
+ }
26
+ targets.add(relation.table);
27
+ }
28
+ }
29
+ const blocked = /* @__PURE__ */ new Set();
30
+ const blockMap = /* @__PURE__ */ new Map();
31
+ const stack = [];
32
+ const reported = /* @__PURE__ */ new Set();
33
+ let capped = false;
34
+ const unblock = (vertex) => {
35
+ blocked.delete(vertex);
36
+ const dependents = blockMap.get(vertex);
37
+ if (dependents === void 0) {
38
+ return;
39
+ }
40
+ for (const dependent of dependents) {
41
+ dependents.delete(dependent);
42
+ if (blocked.has(dependent)) {
43
+ unblock(dependent);
44
+ }
45
+ }
46
+ };
47
+ const emitCurrentCycle = () => {
48
+ const cycle = [...stack];
49
+ if (cycle.length < 2) {
50
+ return;
51
+ }
52
+ let minIndex = 0;
53
+ for (let index = 1; index < cycle.length; index += 1) {
54
+ if (cycle[index] < cycle[minIndex]) {
55
+ minIndex = index;
56
+ }
57
+ }
58
+ const canonical = [...cycle.slice(minIndex), ...cycle.slice(0, minIndex)];
59
+ const key = canonical.join("→");
60
+ if (reported.has(key)) {
61
+ return;
62
+ }
63
+ reported.add(key);
64
+ const displayPath = [...canonical, canonical[0]].join(" → ");
65
+ findings.push(
66
+ emit(circularFk, {
67
+ cacheKey: `circular_fk:${key}`,
68
+ detail: `Circular foreign-key dependency detected: ${displayPath}. This cycle can cause unexpected behavior during DELETE operations.`,
69
+ metadata: {
70
+ cycle: canonical,
71
+ path: displayPath,
72
+ tables: canonical
73
+ }
74
+ })
75
+ );
76
+ if (reported.size >= MAX_CYCLES) {
77
+ capped = true;
78
+ }
79
+ };
80
+ const circuit = (vertex, start, startIndex) => {
81
+ let foundCycle = false;
82
+ stack.push(vertex);
83
+ blocked.add(vertex);
84
+ for (const neighbor of adjacency.get(vertex) ?? []) {
85
+ if (capped) {
86
+ break;
87
+ }
88
+ if (indexOf.get(neighbor) < startIndex) {
89
+ continue;
90
+ }
91
+ if (neighbor === start) {
92
+ emitCurrentCycle();
93
+ foundCycle = true;
94
+ } else if (!blocked.has(neighbor) && circuit(neighbor, start, startIndex)) {
95
+ foundCycle = true;
96
+ }
97
+ }
98
+ if (foundCycle) {
99
+ unblock(vertex);
100
+ } else {
101
+ for (const neighbor of adjacency.get(vertex) ?? []) {
102
+ if (indexOf.get(neighbor) < startIndex) {
103
+ continue;
104
+ }
105
+ let dependents = blockMap.get(neighbor);
106
+ if (dependents === void 0) {
107
+ dependents = /* @__PURE__ */ new Set();
108
+ blockMap.set(neighbor, dependents);
109
+ }
110
+ dependents.add(vertex);
111
+ }
112
+ }
113
+ stack.pop();
114
+ return foundCycle;
115
+ };
116
+ for (let startIndex = 0; startIndex < order.length && !capped; startIndex += 1) {
117
+ blocked.clear();
118
+ blockMap.clear();
119
+ circuit(order[startIndex], order[startIndex], startIndex);
120
+ }
121
+ return findings;
122
+ },
123
+ source: "static",
124
+ title: "Circular foreign-key dependency"
125
+ };
126
+
127
+ export { circularFk as default };
@@ -9,10 +9,28 @@ const formatExamples = (ids, total) => {
9
9
  const list = examples.map((id) => `"${id}"`).join(", ");
10
10
  return more > 0 ? `${list} (+${more.toString()} more)` : list;
11
11
  };
12
+ const resolveReferenceValues = (referencedColumn, targetSample) => {
13
+ if (referencedColumn === "_id") {
14
+ return targetSample.existingIds;
15
+ }
16
+ const values = /* @__PURE__ */ new Set();
17
+ for (const row of targetSample.rows) {
18
+ const value = row[referencedColumn];
19
+ if (value !== null && value !== void 0) {
20
+ values.add(String(value));
21
+ }
22
+ }
23
+ return values;
24
+ };
12
25
  const checkFkRelation = (lint, sample, relation, targetSample) => {
13
26
  if (relation.kind !== "one") {
14
27
  return void 0;
15
28
  }
29
+ if (targetSample.truncated) {
30
+ return void 0;
31
+ }
32
+ const referencedColumn = relation.references;
33
+ const referenceValues = resolveReferenceValues(referencedColumn, targetSample);
16
34
  const fkColumn = relation.field;
17
35
  const danglingIds = [];
18
36
  for (const row of sample.rows) {
@@ -21,16 +39,17 @@ const checkFkRelation = (lint, sample, relation, targetSample) => {
21
39
  continue;
22
40
  }
23
41
  const fkString = String(fkValue);
24
- if (!targetSample.existingIds.has(fkString)) {
42
+ if (!referenceValues.has(fkString)) {
25
43
  danglingIds.push(rowId(row));
26
44
  }
27
45
  }
28
46
  if (danglingIds.length === 0) {
29
47
  return void 0;
30
48
  }
31
- const isTruncated = sample.truncated || targetSample.truncated;
32
- const cap = Math.min(sample.cap, targetSample.cap);
33
- const detail = `Table "${sample.table}": ${danglingIds.length.toString()} sampled row(s) have a dangling FK value in column "${fkColumn}" the referenced "${relation.table}" row does not exist. Row ids: ${formatExamples(danglingIds, danglingIds.length)}${truncatedSuffix(isTruncated, cap, "more rows may exist beyond the window")}.`;
49
+ const isTruncated = sample.truncated;
50
+ const { cap } = sample;
51
+ const referencedLabel = referencedColumn === "_id" ? `"${relation.table}"` : `"${relation.table}"."${referencedColumn}"`;
52
+ const detail = `Table "${sample.table}": ${danglingIds.length.toString()} sampled row(s) have a dangling FK value in column "${fkColumn}" — the referenced ${referencedLabel} row does not exist. Row ids: ${formatExamples(danglingIds, danglingIds.length)}${truncatedSuffix(isTruncated, cap, "more rows may exist beyond the window")}.`;
34
53
  return emit(lint, {
35
54
  cacheKey: `constraint_validator:fk:${sample.table}:${fkColumn}`,
36
55
  detail,
@@ -40,6 +59,7 @@ const checkFkRelation = (lint, sample, relation, targetSample) => {
40
59
  count: danglingIds.length,
41
60
  examples: danglingIds.slice(0, MAX_EXAMPLE_IDS),
42
61
  kind: "fk",
62
+ references: referencedColumn,
43
63
  referencesTable: relation.table,
44
64
  table: sample.table,
45
65
  truncated: isTruncated
@@ -0,0 +1,13 @@
1
+ const dedupeCacheKeys = (findings) => {
2
+ const seen = /* @__PURE__ */ new Map();
3
+ return findings.map((finding) => {
4
+ const occurrence = (seen.get(finding.cacheKey) ?? 0) + 1;
5
+ seen.set(finding.cacheKey, occurrence);
6
+ if (occurrence === 1) {
7
+ return finding;
8
+ }
9
+ return { ...finding, cacheKey: `${finding.cacheKey}:${occurrence.toString()}` };
10
+ });
11
+ };
12
+
13
+ export { dedupeCacheKeys };
@@ -1,3 +1,4 @@
1
+ import { dedupeCacheKeys } from './dedupeCacheKeys-r5B7u_yq.mjs';
1
2
  import { e as emit } from './finding-Dm_zvzS1.mjs';
2
3
 
3
4
  const hardcodedSecret = {
@@ -12,21 +13,16 @@ const hardcodedSecret = {
12
13
  return [];
13
14
  }
14
15
  const findings = [];
15
- const occurrenceCount = /* @__PURE__ */ new Map();
16
16
  for (const secret of context.secretLiterals) {
17
- const baseKey = `${secret.file}:${secret.line.toString()}:${secret.kind}`;
18
- const occurrence = (occurrenceCount.get(baseKey) ?? 0) + 1;
19
- occurrenceCount.set(baseKey, occurrence);
20
- const occurrenceSuffix = occurrence > 1 ? `:${occurrence.toString()}` : "";
21
17
  findings.push(
22
18
  emit(hardcodedSecret, {
23
- cacheKey: `hardcoded_secret:${baseKey}${occurrenceSuffix}`,
19
+ cacheKey: `hardcoded_secret:${secret.file}:${secret.line.toString()}:${secret.kind}`,
24
20
  detail: `A ${secret.kind.replaceAll("_", " ")} (${secret.preview}) is hard-coded at ${secret.file}:${secret.line.toString()}. Move it to \`.dev.vars\` / \`wrangler secret put\` and read it from \`env\`. Rotate the exposed value.`,
25
21
  metadata: { file: secret.file, kind: secret.kind, line: secret.line, preview: secret.preview }
26
22
  })
27
23
  );
28
24
  }
29
- return findings;
25
+ return dedupeCacheKeys(findings);
30
26
  },
31
27
  source: "static",
32
28
  title: "Hard-coded secret in source"
@@ -0,0 +1,60 @@
1
+ import { e as emit } from './finding-Dm_zvzS1.mjs';
2
+
3
+ const HOT_SHARE_THRESHOLD = 0.5;
4
+ const MIN_TOTAL_REQUESTS = 50;
5
+ const hotShard = {
6
+ categories: ["PERFORMANCE"],
7
+ description: "One shard is taking a disproportionate share of a sharded function's traffic, so the busy shard becomes a single-DO bottleneck while its siblings sit idle — the hot-key skew sharding is meant to avoid.",
8
+ facing: "EXTERNAL",
9
+ level: "WARN",
10
+ name: "hot_shard",
11
+ remediation: "Re-shard on a higher-cardinality / more evenly-distributed key, or split the hot entity's state, so request volume spreads across shards instead of concentrating on one.",
12
+ run: (context) => {
13
+ const active = (context.shardTraffic ?? []).filter((shard) => shard.requests > 0);
14
+ const byGroup = /* @__PURE__ */ new Map();
15
+ for (const shard of active) {
16
+ const groupKey = shard.group ?? "";
17
+ const bucket = byGroup.get(groupKey);
18
+ if (bucket === void 0) {
19
+ byGroup.set(groupKey, [shard]);
20
+ } else {
21
+ bucket.push(shard);
22
+ }
23
+ }
24
+ const findings = [];
25
+ for (const [groupKey, groupShards] of byGroup) {
26
+ const totalRequests = groupShards.reduce((sum, shard) => sum + shard.requests, 0);
27
+ if (groupShards.length < 2 || totalRequests < MIN_TOTAL_REQUESTS) {
28
+ continue;
29
+ }
30
+ for (const shard of groupShards) {
31
+ const share = shard.requests / totalRequests;
32
+ if (share < HOT_SHARE_THRESHOLD) {
33
+ continue;
34
+ }
35
+ const scope = shard.group !== void 0 && shard.group !== "" ? `"${shard.group}" ` : "";
36
+ const label = shard.shardKey === "" ? "the root shard" : `shard "${shard.shardKey}"`;
37
+ const percent = Math.round(share * 100);
38
+ findings.push(
39
+ emit(hotShard, {
40
+ cacheKey: `hot_shard:${groupKey}:${shard.shardKey}`,
41
+ detail: `${scope}${label} handled ${shard.requests.toString()} of ${totalRequests.toString()} requests (${percent.toString()}%) across ${groupShards.length.toString()} shards — a hot-key skew. Re-shard on a more evenly-distributed key.`,
42
+ metadata: {
43
+ group: shard.group,
44
+ requests: shard.requests,
45
+ shardCount: groupShards.length,
46
+ shardKey: shard.shardKey,
47
+ share,
48
+ totalRequests
49
+ }
50
+ })
51
+ );
52
+ }
53
+ }
54
+ return findings;
55
+ },
56
+ source: "runtime",
57
+ title: "Hot shard"
58
+ };
59
+
60
+ export { hotShard as default };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/advisor",
3
- "version": "1.0.0-alpha.25",
3
+ "version": "1.0.0-alpha.26",
4
4
  "description": "Schema & query lints (splinter-style advisors) for Lunora, feeding the Studio Advisors view",
5
5
  "keywords": [
6
6
  "advisor",
@@ -46,8 +46,8 @@
46
46
  "access": "public"
47
47
  },
48
48
  "dependencies": {
49
- "@lunora/errors": "1.0.0-alpha.3",
50
- "@lunora/server": "1.0.0-alpha.22"
49
+ "@lunora/errors": "1.0.0-alpha.4",
50
+ "@lunora/server": "1.0.0-alpha.23"
51
51
  },
52
52
  "engines": {
53
53
  "node": "^22.15.0 || >=24.11.0"
@@ -1,84 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const circularFk = {
4
- categories: ["SCHEMA"],
5
- description: "A chain of foreign-key relations forms a cycle (e.g. A → B → C → A). Circular FK dependencies can cause unexpected behavior during DELETE operations — a CASCADE chain may loop indefinitely, and even a RESTRICT cycle prevents deletion of any row in the loop without temporarily disabling constraints.",
6
- facing: "INTERNAL",
7
- level: "WARN",
8
- name: "circular_fk",
9
- remediation: "Remove or break the cycle by dropping at least one FK relation from the loop. Consider replacing the circular dependency with a nullable FK and explicit application logic, or restructure the schema using a junction table.",
10
- run: (context) => {
11
- const findings = [];
12
- const edges = /* @__PURE__ */ new Map();
13
- for (const table of context.schema.tables) {
14
- for (const relation of table.relations) {
15
- if (relation.kind !== "one") {
16
- continue;
17
- }
18
- let targets = edges.get(table.name);
19
- if (targets === void 0) {
20
- targets = /* @__PURE__ */ new Set();
21
- edges.set(table.name, targets);
22
- }
23
- targets.add(relation.table);
24
- }
25
- }
26
- const onStack = /* @__PURE__ */ new Set();
27
- const reported = /* @__PURE__ */ new Set();
28
- const reportBackEdge = (neighbor, path) => {
29
- const cycleStart = path.indexOf(neighbor);
30
- if (cycleStart === -1) {
31
- return;
32
- }
33
- const cycle = path.slice(cycleStart);
34
- if (cycle.length < 2) {
35
- return;
36
- }
37
- let minIndex = 0;
38
- for (let index = 1; index < cycle.length; index += 1) {
39
- if (cycle[index] < cycle[minIndex]) {
40
- minIndex = index;
41
- }
42
- }
43
- const canonical = [...cycle.slice(minIndex), ...cycle.slice(0, minIndex)];
44
- const key = canonical.join("→");
45
- if (reported.has(key)) {
46
- return;
47
- }
48
- reported.add(key);
49
- const displayPath = [...canonical, canonical[0]].join(" → ");
50
- findings.push(
51
- emit(circularFk, {
52
- cacheKey: `circular_fk:${key}`,
53
- detail: `Circular foreign-key dependency detected: ${displayPath}. This cycle can cause unexpected behavior during DELETE operations.`,
54
- metadata: {
55
- cycle: canonical,
56
- path: displayPath,
57
- tables: canonical
58
- }
59
- })
60
- );
61
- };
62
- const dfs = (node, path) => {
63
- onStack.add(node);
64
- path.push(node);
65
- for (const neighbor of edges.get(node) ?? []) {
66
- if (onStack.has(neighbor)) {
67
- reportBackEdge(neighbor, path);
68
- } else {
69
- dfs(neighbor, path);
70
- }
71
- }
72
- path.pop();
73
- onStack.delete(node);
74
- };
75
- for (const table of context.schema.tables) {
76
- dfs(table.name, []);
77
- }
78
- return findings;
79
- },
80
- source: "static",
81
- title: "Circular foreign-key dependency"
82
- };
83
-
84
- export { circularFk as default };
@@ -1,48 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const HOT_SHARE_THRESHOLD = 0.5;
4
- const MIN_TOTAL_REQUESTS = 50;
5
- const hotShard = {
6
- categories: ["PERFORMANCE"],
7
- description: "One shard is taking a disproportionate share of a sharded function's traffic, so the busy shard becomes a single-DO bottleneck while its siblings sit idle — the hot-key skew sharding is meant to avoid.",
8
- facing: "EXTERNAL",
9
- level: "WARN",
10
- name: "hot_shard",
11
- remediation: "Re-shard on a higher-cardinality / more evenly-distributed key, or split the hot entity's state, so request volume spreads across shards instead of concentrating on one.",
12
- run: (context) => {
13
- const active = (context.shardTraffic ?? []).filter((shard) => shard.requests > 0);
14
- const totalRequests = active.reduce((sum, shard) => sum + shard.requests, 0);
15
- if (active.length < 2 || totalRequests < MIN_TOTAL_REQUESTS) {
16
- return [];
17
- }
18
- const findings = [];
19
- for (const shard of active) {
20
- const share = shard.requests / totalRequests;
21
- if (share < HOT_SHARE_THRESHOLD) {
22
- continue;
23
- }
24
- const scope = shard.group !== void 0 && shard.group !== "" ? `"${shard.group}" ` : "";
25
- const label = shard.shardKey === "" ? "the root shard" : `shard "${shard.shardKey}"`;
26
- const percent = Math.round(share * 100);
27
- findings.push(
28
- emit(hotShard, {
29
- cacheKey: `hot_shard:${shard.group ?? ""}:${shard.shardKey}`,
30
- detail: `${scope}${label} handled ${shard.requests.toString()} of ${totalRequests.toString()} requests (${percent.toString()}%) across ${active.length.toString()} shards — a hot-key skew. Re-shard on a more evenly-distributed key.`,
31
- metadata: {
32
- group: shard.group,
33
- requests: shard.requests,
34
- shardCount: active.length,
35
- shardKey: shard.shardKey,
36
- share,
37
- totalRequests
38
- }
39
- })
40
- );
41
- }
42
- return findings;
43
- },
44
- source: "runtime",
45
- title: "Hot shard"
46
- };
47
-
48
- export { hotShard as default };