-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy patheslint.config.js
More file actions
277 lines (274 loc) · 13.3 KB
/
Copy patheslint.config.js
File metadata and controls
277 lines (274 loc) · 13.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
import js from '@eslint/js'
import globals from 'globals'
import regexpPlugin from 'eslint-plugin-regexp'
import tseslint from 'typescript-eslint'
const ALT_CHART_LIB_PATHS = [
{ name: 'chart.js', message: 'Use Recharts via ChartPrimitives instead.' },
{ name: 'highcharts', message: 'Use Recharts via ChartPrimitives instead.' },
{ name: 'd3', message: 'Use Recharts via ChartPrimitives instead.' },
{ name: 'victory', message: 'Use Recharts via ChartPrimitives instead.' },
{ name: '@nivo/core', message: 'Use Recharts via ChartPrimitives instead.' },
{ name: 'plotly.js', message: 'Use Recharts via ChartPrimitives instead.' },
]
const ALT_CHART_LIB_PATTERNS = [
{ group: ['chart.js/*'], message: 'Use Recharts via ChartPrimitives instead.' },
{ group: ['highcharts/*'], message: 'Use Recharts via ChartPrimitives instead.' },
{ group: ['d3-*'], message: 'Use Recharts via ChartPrimitives instead.' },
{ group: ['victory-*'], message: 'Use Recharts via ChartPrimitives instead.' },
{ group: ['@nivo/*'], message: 'Use Recharts via ChartPrimitives instead.' },
{ group: ['plotly.js-*'], message: 'Use Recharts via ChartPrimitives instead.' },
]
export default tseslint.config(
{
ignores: [
'dist/',
'node_modules/',
'apps/**/dist/',
'packages/**/dist/',
// Auto-generated hey-api client — never hand-edited; lint warnings
// would only show up to be regenerated away on the next `pnpm gen`.
'packages/api-client-generated/src/generated/**',
],
},
{
// CLI commands must be fully non-interactive. readline is only allowed in
// init.ts as a human convenience — all init values are also passable via flags.
files: ['packages/canonry/src/commands/**/*.ts', 'packages/canonry/src/cli-commands/**/*.ts'],
ignores: ['packages/canonry/src/commands/init.ts'],
rules: {
'no-restricted-imports': ['error', {
paths: [
{ name: 'node:readline', message: 'CLI commands must be non-interactive. Accept values via flags, env vars, or config.yaml.' },
{ name: 'readline', message: 'CLI commands must be non-interactive. Accept values via flags, env vars, or config.yaml.' },
],
}],
},
},
{
// Vocabulary enforcement: per AGENTS.md "Vocabulary (Critical)", user-facing
// labels for `answer_mentioned` must say "mentioned" / "not-mentioned" and
// labels for `citation_state` must say "cited" / "not-cited". The legacy
// umbrella term "visibility" is permitted only when explicitly disambiguated
// (e.g. "Visibility Gap (Citations + Mentions)"). The literals below are
// unambiguous user-facing labels that conflate the two signals — bare
// `'visible'` is excluded because it has legitimate uses (DOM API, the
// legacy `VisibilityState` enum value) that lint cannot disambiguate.
files: [
'packages/canonry/src/commands/**/*.ts',
'packages/canonry/src/cli-commands/**/*.ts',
'packages/api-routes/src/**/*.ts',
'apps/web/src/**/*.ts',
'apps/web/src/**/*.tsx',
],
rules: {
'no-restricted-syntax': ['error', {
selector: "Literal[value=/^(not-vis|visibility run|visibility sweep|visibility report|answer rate|answer-rate|answerRate)$/]",
message: 'Use canonical AEO vocabulary: "mentioned" / "not-mentioned" for answer-text presence, "cited" / "not-cited" for source-list presence. See AGENTS.md "Vocabulary (Critical)".',
}, {
selector: "Literal[value=/^(paid mentions|paid citations|ad mentions|ad citations|sponsored mentions|sponsored citations|paid-mention|paid-citation)$/]",
message: 'Paid-surface metrics are "paid" / "sponsored" (impressions, clicks, spend) — never combined with "mentioned"/"cited", which mean organic answer-text / source-list presence. See AGENTS.md "Vocabulary (Critical)".',
}],
},
},
{
// Drift guard: GA4 dimension/metric names must come from `GA4_DIMENSIONS` /
// `GA4_METRICS` in `packages/integration-google-analytics/src/constants.ts`.
// CI broke once when source and test drifted on `sessionDefaultChannelGroup`
// vs `…Grouping`; the constant makes that class of failure impossible.
files: ['packages/integration-google-analytics/src/**/*.ts'],
ignores: ['packages/integration-google-analytics/src/constants.ts'],
rules: {
'no-restricted-syntax': ['error', {
selector: "Literal[value=/^(sessionSource|sessionMedium|sessionManualSource|sessionManualMedium|firstUserSource|firstUserMedium|sessionDefaultChannelGroup|sessionDefaultChannelGrouping|landingPagePlusQueryString)$/]",
message: 'Use GA4_DIMENSIONS from ./constants.ts — never inline raw dimension names. See packages/integration-google-analytics/src/constants.ts.',
}],
},
},
{
// Drift guard: AI-engine hostnames in production code must come from
// `AI_ENGINE_DOMAINS` in `packages/contracts/src/ai-engines.ts`. Tests are
// exempt because fixtures are local to their assertions and don't drift
// across files.
files: [
'packages/canonry/src/**/*.ts',
'packages/api-routes/src/**/*.ts',
'packages/provider-*/src/**/*.ts',
'packages/integration-*/src/**/*.ts',
'packages/intelligence/src/**/*.ts',
'apps/**/src/**/*.ts',
'apps/**/src/**/*.tsx',
],
ignores: ['packages/contracts/src/ai-engines.ts'],
rules: {
'no-restricted-syntax': ['error', {
selector: "Literal[value=/^(openai\\.com|chatgpt\\.com|claude\\.ai|perplexity\\.ai|gemini\\.google\\.com|bard\\.google\\.com|copilot\\.microsoft\\.com|meta\\.ai|grok\\.com|you\\.com|phind\\.com|anthropic\\.com|googleapis\\.com|vertexaisearch\\.cloud\\.google\\.com)$/]",
message: 'Use AI_ENGINE_DOMAINS / AI_PROVIDER_INFRA_DOMAINS / ANTHROPIC_API_DOMAIN / GOOGLE_APIS_DOMAIN / VERTEX_AI_SEARCH_PROXY_DOMAIN from @ainyc/canonry-contracts — never inline raw AI-provider hostnames in production code.',
}],
},
},
{
files: ['**/*.js', '**/*.ts', '**/*.tsx'],
extends: [regexpPlugin.configs['flat/recommended']],
},
{
files: ['**/*.js'],
extends: [js.configs.recommended],
languageOptions: {
globals: {
...globals.node,
},
},
rules: {
'no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' }],
},
},
{
files: ['**/*.ts', '**/*.tsx'],
extends: [js.configs.recommended, ...tseslint.configs.recommended],
languageOptions: {
globals: {
...globals.node,
},
},
rules: {
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' }],
'no-warning-comments': ['warn', { terms: ['todo', 'fixme', 'hack', 'xxx'], location: 'start' }],
},
},
{
// Type-aware rules — limited to `src/` because test files aren't in package tsconfigs'
// `include` and would trigger projectService parsing errors. Adds @typescript-eslint/no-unnecessary-condition
// to catch always-true/always-false comparisons (e.g. checking !== undefined on a narrowed type).
files: ['**/src/**/*.ts', '**/src/**/*.tsx'],
extends: [...tseslint.configs.recommendedTypeChecked],
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
// Kept as `warn`: 363 pre-existing findings, mostly defensive `?.`/`??` noise. Drain
// incrementally before flipping to `error`.
'@typescript-eslint/no-unnecessary-condition': 'warn',
// Soundness rules promoted to error — catch real bug classes (forgotten awaits,
// misused promises, `any` leaking into typed code, broken template-string output,
// unbound methods, awaiting non-thenables).
'@typescript-eslint/no-misused-promises': 'error',
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-unsafe-assignment': 'error',
'@typescript-eslint/no-unsafe-member-access': 'error',
'@typescript-eslint/no-unsafe-call': 'error',
'@typescript-eslint/no-unsafe-return': 'error',
'@typescript-eslint/no-unsafe-argument': 'error',
'@typescript-eslint/no-base-to-string': 'error',
'@typescript-eslint/await-thenable': 'error',
'@typescript-eslint/unbound-method': 'error',
'@typescript-eslint/no-duplicate-type-constituents': 'error',
'@typescript-eslint/no-implied-eval': 'error',
'@typescript-eslint/prefer-promise-reject-errors': 'error',
'@typescript-eslint/restrict-plus-operands': 'error',
// Lower-value or noisy — left off for now; revisit after the soundness set is drained.
'@typescript-eslint/restrict-template-expressions': 'off',
'@typescript-eslint/no-redundant-type-constituents': 'off',
'@typescript-eslint/require-await': 'off',
'@typescript-eslint/only-throw-error': 'off',
'@typescript-eslint/no-unnecessary-type-assertion': 'off',
},
},
{
// ChartPrimitives is the only file allowed to import directly from recharts.
// All other web files must use ChartPrimitives and may not use alternative chart libs.
files: ['apps/web/**/*.ts', 'apps/web/**/*.tsx'],
ignores: ['apps/web/src/components/shared/ChartPrimitives.tsx'],
languageOptions: {
globals: {
...globals.browser,
},
},
rules: {
'no-restricted-imports': ['error', {
paths: [
...ALT_CHART_LIB_PATHS,
{ name: 'recharts', message: 'Import from ChartPrimitives.js instead of recharts directly.' },
],
patterns: [
...ALT_CHART_LIB_PATTERNS,
{ group: ['recharts/*'], message: 'Import from ChartPrimitives.js instead of recharts directly.' },
],
}],
},
},
{
// ChartPrimitives itself can import recharts but not alternative chart libs
files: ['apps/web/src/components/shared/ChartPrimitives.tsx'],
languageOptions: {
globals: {
...globals.browser,
},
},
rules: {
'no-restricted-imports': ['error', {
paths: ALT_CHART_LIB_PATHS,
patterns: ALT_CHART_LIB_PATTERNS,
}],
},
},
{
// SDK enforcement: every web call into the canonry API must flow through
// the generated `@ainyc/canonry-api-client` SDK (raw call or TanStack
// helper), with auth + 401/403 handling provided by the shared
// `heyClient` from `apps/web/src/api.ts`. Raw `fetch()` to API URLs
// bypasses every spec-derived contract and the auth interceptor.
//
// The two thin shim files (`api.ts` for the typed wrappers around SDK
// calls; `api-aero.ts` for the SSE prompt stream + transcript reads
// that ride on `EventSource`) are excluded — those are the only places
// raw `fetch()` is legitimate. Tests are also excluded because they
// stub `globalThis.fetch` via `vi.stubGlobal`.
files: ['apps/web/src/**/*.ts', 'apps/web/src/**/*.tsx'],
ignores: [
'apps/web/src/api.ts',
'apps/web/src/api-aero.ts',
],
rules: {
'no-restricted-syntax': ['error', {
selector: "CallExpression[callee.name='fetch']",
message: 'Use the generated `@ainyc/canonry-api-client` SDK (via `heyClient` from `apps/web/src/api.ts`) instead of raw `fetch()`. Spec drift + auth interceptor bypass is silent and pernicious. For an endpoint missing a typed DTO, add a Zod schema in `packages/contracts` and flip the route to `jsonResponse(...)` first.',
}, {
selector: "NewExpression[callee.name='XMLHttpRequest']",
message: 'Use the generated `@ainyc/canonry-api-client` SDK (via `heyClient` from `apps/web/src/api.ts`) instead of `XMLHttpRequest`.',
}],
},
},
{
// Analog of the apps/web SDK enforcement, for the CLI. Every CLI / job
// runner / server-internal call into the canonry API must go through
// `ApiClient` (which delegates to the generated SDK via `invoke()`),
// not raw `fetch()`. The only legitimate raw fetches are inside
// `client.ts` itself: the `/health` probe (bootstrap check that lives
// outside `/api/v1`) and the SSE `streamPost()` (the SDK can't
// represent text/event-stream cleanly). Both are bounded by file.
files: ['packages/canonry/src/**/*.ts'],
ignores: [
// `ApiClient`'s `/health` probe + SSE prompt stream.
'packages/canonry/src/client.ts',
// External HTTP — not the canonry API:
// - daemon.ts probes localhost `/health` for serve-readiness
// - sitemap-parser.ts fetches the user's own sitemap.xml URL
// - telemetry.ts POSTs to the public telemetry collector
// - update-check.ts polls npm dist-tags
'packages/canonry/src/commands/daemon.ts',
'packages/canonry/src/sitemap-parser.ts',
'packages/canonry/src/telemetry.ts',
'packages/canonry/src/update-check.ts',
],
rules: {
'no-restricted-syntax': ['error', {
selector: "CallExpression[callee.name='fetch']",
message: 'Use the generated `@ainyc/canonry-api-client` SDK via `ApiClient` / `createApiClient()` (which routes through `invoke()` for tracing, CliError mapping, and the base-path probe) instead of raw `fetch()`. If you genuinely need raw `fetch()` for an external (non-canonry) HTTP call, add the file to the `ignores` list in `eslint.config.js` with a one-line comment naming the external service.',
}],
},
},
)