From 0d761744df672110fc3a1abec28a14c80db0d79b Mon Sep 17 00:00:00 2001 From: openhands Date: Tue, 24 Feb 2026 20:44:17 +0000 Subject: [PATCH 1/2] fix: Add architectural lint rules with agent-friendly error messages (#232) - landing/eslint.config.js: ban imports from web-app paths (rule 1), direct RPC clients from viem/@wagmi/vue (rule 2), and axios (rule 4) - web-app/eslint.config.js: ban string interpolation inside GraphQL query/mutation property values (rule 3); fixes 4 pre-existing violations in usePositionDashboard, usePositions, useSnatchNotifications, useWalletDashboard by migrating to variables: {} pattern - services/ponder/eslint.config.js: ban findMany() calls that lack a limit parameter to prevent unbounded indexed-data growth (rule 5) All error messages follow the [what is wrong][rule][how to fix][where to read more] template so agents and humans fix on the first try. Co-Authored-By: Claude Sonnet 4.6 --- landing/eslint.config.js | 50 +++++++++++++++++++ services/ponder/eslint.config.js | 13 +++++ web-app/eslint.config.js | 15 ++++++ .../src/composables/usePositionDashboard.ts | 5 +- web-app/src/composables/usePositions.ts | 5 +- .../src/composables/useSnatchNotifications.ts | 13 ++--- web-app/src/composables/useWalletDashboard.ts | 7 +-- 7 files changed, 95 insertions(+), 13 deletions(-) diff --git a/landing/eslint.config.js b/landing/eslint.config.js index add803f..83f4fc0 100644 --- a/landing/eslint.config.js +++ b/landing/eslint.config.js @@ -13,6 +13,56 @@ export default [ }, ...pluginVue.configs['flat/essential'], ...vueTsEslintConfig(), + { + name: 'arch/landing-import-restrictions', + rules: { + 'no-restricted-imports': [ + 'error', + { + patterns: [ + { + group: ['**/web-app/**', '@harb/web-app/*'], + message: + 'Landing component imports from web-app. Landing must not depend on web-app. Move shared code to kraiken-lib or a shared/ package. See docs/ARCHITECTURE.md.', + }, + ], + paths: [ + { + name: 'viem', + importNames: ['createPublicClient', 'publicActions'], + message: + 'Landing component imports a direct RPC client from viem. Landing must not make direct RPC calls from the browser — this will kill any node under load. Use Ponder GraphQL at /api/graphql instead. See docs/UX-DECISIONS.md.', + }, + { + name: '@wagmi/vue', + importNames: ['usePublicClient'], + message: + 'Landing component imports usePublicClient from @wagmi/vue. Landing must not make direct RPC calls from the browser — this will kill any node under load. Use Ponder GraphQL at /api/graphql instead. See docs/UX-DECISIONS.md.', + }, + { + name: 'axios', + message: + 'Landing component imports axios. Landing must not use axios — it increases bundle size for no benefit. Use native fetch() with the /api/graphql proxy instead.', + }, + ], + }, + ], + }, + }, + { + name: 'arch/graphql-no-interpolation', + rules: { + 'no-restricted-syntax': [ + 'error', + { + selector: + "Property[key.name='query'] > TemplateLiteral[expressions.length>0], Property[key.name='mutation'] > TemplateLiteral[expressions.length>0]", + message: + 'String interpolation used in a GraphQL query or mutation string. GraphQL queries must not use string interpolation — it bypasses type-checking and is unsafe. Use the `variables` parameter in the fetch body instead. Example: `variables: { holder: address }`. See PR #191 for the pattern.', + }, + ], + }, + }, { name: 'app/custom-rules', rules: { diff --git a/services/ponder/eslint.config.js b/services/ponder/eslint.config.js index dad4514..8b6deed 100644 --- a/services/ponder/eslint.config.js +++ b/services/ponder/eslint.config.js @@ -72,5 +72,18 @@ export default [ 'max-statements': 'off', }, }, + { + name: 'arch/ponder-bounded-queries', + rules: { + 'no-restricted-syntax': [ + 'error', + { + selector: "CallExpression[callee.property.name='findMany']:not(:has(Property[key.name='limit']))", + message: + "Ponder findMany() called without a limit parameter. Unbounded queries will grow without limit as the chain is indexed — never use findMany() without a limit. Always specify `limit` in the options object. Use the ring buffer pattern from docs/ARCHITECTURE.md.", + }, + ], + }, + }, eslintConfigPrettier, ]; diff --git a/web-app/eslint.config.js b/web-app/eslint.config.js index b194ff3..9cf877b 100644 --- a/web-app/eslint.config.js +++ b/web-app/eslint.config.js @@ -81,6 +81,21 @@ export default [ }, }, + { + name: 'arch/graphql-no-interpolation', + rules: { + 'no-restricted-syntax': [ + 'error', + { + selector: + "Property[key.name='query'] > TemplateLiteral[expressions.length>0], Property[key.name='mutation'] > TemplateLiteral[expressions.length>0]", + message: + 'String interpolation used in a GraphQL query or mutation string. GraphQL queries must not use string interpolation — it bypasses type-checking and is unsafe. Use the `variables` parameter in the fetch body instead. Example: `variables: { holder: address }`. See PR #191 for the pattern.', + }, + ], + }, + }, + { name: 'app/tests-override', files: ['src/**/__tests__/**/*.ts', 'src/**/__tests__/**/*.tsx'], diff --git a/web-app/src/composables/usePositionDashboard.ts b/web-app/src/composables/usePositionDashboard.ts index 2846d67..cac7d55 100644 --- a/web-app/src/composables/usePositionDashboard.ts +++ b/web-app/src/composables/usePositionDashboard.ts @@ -107,8 +107,8 @@ export function usePositionDashboard(positionId: Ref) { const res = await axios.post( endpoint, { - query: `query PositionDashboard { - positions(id: "${id}") { + query: `query PositionDashboard($id: String!) { + positions(id: $id) { id owner share @@ -142,6 +142,7 @@ export function usePositionDashboard(positionId: Ref) { } } }`, + variables: { id }, }, { timeout: GRAPHQL_TIMEOUT_MS } ); diff --git a/web-app/src/composables/usePositions.ts b/web-app/src/composables/usePositions.ts index 9855c84..0b38439 100644 --- a/web-app/src/composables/usePositions.ts +++ b/web-app/src/composables/usePositions.ts @@ -194,8 +194,8 @@ export async function loadMyClosedPositions(chainId: number, endpointOverride: s const res = await axios.post( targetEndpoint, { - query: `query ClosedPositions { - positionss(where: { status: "Closed", owner: "${account.address?.toLowerCase()}" }, limit: 1000) { + query: `query ClosedPositions($owner: String!) { + positionss(where: { status: "Closed", owner: $owner }, limit: 1000) { items { id lastTaxTime @@ -214,6 +214,7 @@ export async function loadMyClosedPositions(chainId: number, endpointOverride: s } } }`, + variables: { owner: account.address?.toLowerCase() ?? '' }, }, { timeout: GRAPHQL_TIMEOUT_MS } ); diff --git a/web-app/src/composables/useSnatchNotifications.ts b/web-app/src/composables/useSnatchNotifications.ts index 62afee4..bb555ef 100644 --- a/web-app/src/composables/useSnatchNotifications.ts +++ b/web-app/src/composables/useSnatchNotifications.ts @@ -40,16 +40,16 @@ function setLastSeen(address: string, timestamp: string): void { async function fetchSnatchEvents(endpoint: string, address: string, since: string | null): Promise { const holder = address.toLowerCase(); - const whereClause = - since !== null - ? `{ holder: "${holder}", type: "snatch_out", timestamp_gt: "${since}" }` - : `{ holder: "${holder}", type: "snatch_out" }`; + const where: { holder: string; type: string; timestamp_gt?: string } = { holder, type: 'snatch_out' }; + if (since !== null) { + where.timestamp_gt = since; + } const res = await axios.post( endpoint, { - query: `query SnatchEvents { + query: `query SnatchEvents($where: transactionsFilter!) { transactionss( - where: ${whereClause} + where: $where orderBy: "timestamp" orderDirection: "desc" limit: 20 @@ -57,6 +57,7 @@ async function fetchSnatchEvents(endpoint: string, address: string, since: strin items { id timestamp tokenAmount ethAmount txHash } } }`, + variables: { where }, }, { timeout: GRAPHQL_TIMEOUT_MS } ); diff --git a/web-app/src/composables/useWalletDashboard.ts b/web-app/src/composables/useWalletDashboard.ts index b455466..cc91405 100644 --- a/web-app/src/composables/useWalletDashboard.ts +++ b/web-app/src/composables/useWalletDashboard.ts @@ -67,8 +67,8 @@ export function useWalletDashboard(address: Ref) { const res = await axios.post( endpoint, { - query: `query WalletDashboard { - holders(address: "${addr}") { + query: `query WalletDashboard($addr: String!) { + holders(address: $addr) { address balance totalEthSpent @@ -88,7 +88,7 @@ export function useWalletDashboard(address: Ref) { holderCount } } - positionss(where: { owner: "${addr}" }, limit: 1000) { + positionss(where: { owner: $addr }, limit: 1000) { items { id share @@ -106,6 +106,7 @@ export function useWalletDashboard(address: Ref) { } } }`, + variables: { addr }, }, { timeout: GRAPHQL_TIMEOUT_MS } ); From ee1079176403bb007d3bad22f5ffe5c9c366685e Mon Sep 17 00:00:00 2001 From: openhands Date: Tue, 24 Feb 2026 20:45:02 +0000 Subject: [PATCH 2/2] fix: Add architectural lint rules with agent-friendly error messages (#232) --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index ac76b61..3d5d096 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "harb-worktree-207", + "name": "harb-worktree-232", "lockfileVersion": 3, "requires": true, "packages": {