mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-11 12:09:03 +00:00
feat(lint): P7 enforcement gates (no-new-throw-error + max-lines) + docs
Epic P7. Locks in the epic's gains with ESLint gates so the adoption work does not regress. - ccs/no-new-throw-error (error): custom flat-config rule that flags NEW throw new Error(...) outside a generated baseline allowlist. Forces the typed-error taxonomy (src/errors/error-types.ts). Existing 338 sites are grandfathered in eslint-rules/throw-error-baseline.json; only NEW violations error. Rule normalizes the filename to repo-root-relative to match baseline keys regardless of how ESLint reports paths. - max-lines (warn, 400, skipBlankLines/skipComments): warns on files over 400 LOC (goal of P5/P6 god-file splits). Currently 51 warnings on the not-yet-split god-files (P6 territory). - scripts/generate-throw-error-baseline.js: emits the allowlist from raw source (superset of real throws; never undercounts). Run after intentionally grandfathering a site, or quarterly to prune. - tests/unit/eslint-rules/no-throw-new-error.test.ts: rule logic (flags off-allowlist, passes on-allowlist/typed/rethrow, line-sensitivity). - docs/code-standards.md: Lint Enforcement Gates section. - docs/logging-contract.md: error.code -> ExitCode table (from P4). validate + validate:ci-parity green.
This commit is contained in:
1 parent
919be3c722
commit
2f94f35ec3
7 files changed
+642
No files matched your search
@@ -730,6 +730,18 @@ This pattern is used in:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Lint Enforcement Gates
|
||||||
|
|
||||||
|
Two ESLint gates (`eslint.config.mjs`) lock in the maintainability epic's gains:
|
||||||
|
|
||||||
|
- **`ccs/no-new-throw-error`** (error): flags new `throw new Error(...)`. Use a typed error from `src/errors/error-types.ts` (`AuthError`, `ConfigError`, `ProfileError`, `ProviderError`, `NetworkError`, `ProxyError`, `MigrationError`, `ValidationError`, `RetryableError`) so `handleError` emits a differentiated exit code. Existing ~340 sites are grandfathered in `eslint-rules/throw-error-baseline.json`; only **new** violations error. Regenerate the baseline when intentionally grandfathering a new site, or quarterly to prune converted entries:
|
||||||
|
```bash
|
||||||
|
node scripts/generate-throw-error-baseline.js
|
||||||
|
```
|
||||||
|
- **`max-lines`** (warn, 400): warns on source files over 400 lines (`skipBlankLines`, `skipComments`). Split via the Monster File Splitting methodology above (barrel `index.ts` preserves the public API).
|
||||||
|
|
||||||
|
When the no-throw rule blocks a change, prefer converting to the matching typed error. Only add to the baseline when the throw is genuinely out of scope to convert (and regenerate the baseline so the entry is explicit, not silent).
|
||||||
|
|
||||||
## Related Documentation
|
## Related Documentation
|
||||||
|
|
||||||
- [Codebase Summary](./codebase-summary.md) - Full directory structure
|
- [Codebase Summary](./codebase-summary.md) - Full directory structure
|
||||||
|
|||||||
@@ -146,6 +146,25 @@ Use `stage()` whenever the entry corresponds to one of the canonical lifecycle s
|
|||||||
|
|
||||||
Default level is `info`. Configure via `logging.level` in `~/.ccs/config.yaml`. Streaming providers MUST gate per-chunk metrics behind `debug`.
|
Default level is `info`. Configure via `logging.level` in `~/.ccs/config.yaml`. Streaming providers MUST gate per-chunk metrics behind `debug`.
|
||||||
|
|
||||||
|
## `error.code` values (exit codes)
|
||||||
|
|
||||||
|
Typed errors (`src/errors/error-types.ts`) carry an `ExitCode` that `handleError` propagates to `process.exit`. Log readers can branch on `error.code` for differentiated handling. The full mapping lives in `src/errors/exit-codes.ts`; the per-class assignment:
|
||||||
|
|
||||||
|
| Typed class | ExitCode | Value |
|
||||||
|
|---|---|---:|
|
||||||
|
| `ConfigError` | `CONFIG_ERROR` | 2 |
|
||||||
|
| `NetworkError` | `NETWORK_ERROR` | 3 (recoverable) |
|
||||||
|
| `AuthError` | `AUTH_ERROR` | 4 |
|
||||||
|
| `BinaryError` | `BINARY_ERROR` | 5 |
|
||||||
|
| `ProviderError` | `PROVIDER_ERROR` | 6 (recoverable) |
|
||||||
|
| `ProfileError` | `PROFILE_ERROR` | 7 |
|
||||||
|
| `ProxyError` | `PROXY_ERROR` | 8 |
|
||||||
|
| `MigrationError` | `MIGRATION_ERROR` | 9 |
|
||||||
|
| `UserAbortError` | `USER_ABORT` | 130 |
|
||||||
|
| `ValidationError`, `RetryableError` | `GENERAL_ERROR` | 1 |
|
||||||
|
|
||||||
|
New throws must use a typed class (enforced by `ccs/no-new-throw-error`, see `docs/code-standards.md`). Redaction scrubs credential token shapes in both context values and message strings, so routing errors into the logger is safe — but keep messages clean prose and put sensitive data in context under a sensitive key (auto-redacted).
|
||||||
|
|
||||||
## Backward Compatibility
|
## Backward Compatibility
|
||||||
|
|
||||||
- All new `LogEntry` fields (`requestId`, `stage`, `latencyMs`, `error`) are optional. Old readers ignore them.
|
- All new `LogEntry` fields (`requestId`, `stage`, `latencyMs`, `error`) are optional. Old readers ignore them.
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* Custom ESLint rule: disallow `throw new Error(...)` outside a baseline allowlist.
|
||||||
|
*
|
||||||
|
* P7 enforcement gate. Forces new error sites to use the typed-error taxonomy
|
||||||
|
* (src/errors/error-types.ts: AuthError, ConfigError, ProfileError, ProviderError,
|
||||||
|
* ...) so handleError emits differentiated exit codes. Existing ~400 sites are
|
||||||
|
* grandfathered via a generated baseline (scripts/generate-throw-error-baseline.js
|
||||||
|
* -> eslint-rules/throw-error-baseline.json); only NEW violations are reported.
|
||||||
|
*
|
||||||
|
* Detects `throw new Error(...)` (NewExpression with callee name 'Error').
|
||||||
|
* Typed subclasses (throw new ConfigError(...)) and re-throws are allowed.
|
||||||
|
*
|
||||||
|
* Option: { allowlist: string[] } — entries are `${relativePath}:${line}` keys.
|
||||||
|
* The relative path matches ESLint's context filename (relative to the repo root
|
||||||
|
* when eslint is invoked from the root). Line drift after edits above an
|
||||||
|
* allowlisted site causes a false positive until the baseline is regenerated;
|
||||||
|
* quarterly pruning keeps it accurate.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
function isNewErrorExpression(node) {
|
||||||
|
return (
|
||||||
|
node !== null &&
|
||||||
|
node !== undefined &&
|
||||||
|
node.type === 'NewExpression' &&
|
||||||
|
node.callee !== null &&
|
||||||
|
node.callee !== undefined &&
|
||||||
|
node.callee.type === 'Identifier' &&
|
||||||
|
node.callee.name === 'Error'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
meta: {
|
||||||
|
type: 'problem',
|
||||||
|
docs: {
|
||||||
|
description:
|
||||||
|
'Disallow throw new Error(...) outside the baseline; use a typed error from src/errors/error-types.ts.',
|
||||||
|
},
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
allowlist: { type: 'array', items: { type: 'string' } },
|
||||||
|
},
|
||||||
|
additionalProperties: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
messages: {
|
||||||
|
unexpected:
|
||||||
|
"Unexpected throw new Error(...). Use a typed error from src/errors/error-types.ts (AuthError, ConfigError, ProfileError, ProviderError, ...) so handleError emits a differentiated exit code, or regenerate the baseline via 'node scripts/generate-throw-error-baseline.js'.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create(context) {
|
||||||
|
const options = context.options[0] || {};
|
||||||
|
const allowlist = new Set(options.allowlist || []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
ThrowStatement(node) {
|
||||||
|
if (!isNewErrorExpression(node.argument)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const filename = context.getFilename();
|
||||||
|
// Normalize to a repo-root-relative path so the baseline keys (which are
|
||||||
|
// relative, e.g. 'src/auth/profile-registry.ts') match regardless of
|
||||||
|
// whether ESLint reports an absolute or relative filename.
|
||||||
|
const normalized = path.isAbsolute(filename)
|
||||||
|
? path.relative(process.cwd(), filename)
|
||||||
|
: filename;
|
||||||
|
const line = node.loc && node.loc.start ? node.loc.start.line : -1;
|
||||||
|
const key = `${normalized}:${line}`;
|
||||||
|
if (allowlist.has(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
context.report({ node, messageId: 'unexpected' });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,340 @@
|
|||||||
|
[
|
||||||
|
"src/api/services/local-runtime-readiness.ts:68",
|
||||||
|
"src/api/services/openrouter-catalog.ts:72",
|
||||||
|
"src/api/services/profile-lifecycle-service.ts:127",
|
||||||
|
"src/api/services/profile-lifecycle-service.ts:73",
|
||||||
|
"src/api/services/profile-lifecycle-service.ts:88",
|
||||||
|
"src/api/services/profile-writer.ts:364",
|
||||||
|
"src/api/services/profile-writer.ts:415",
|
||||||
|
"src/bin/ccsxp-runtime.ts:90",
|
||||||
|
"src/channels/official-channels-store.ts:291",
|
||||||
|
"src/channels/official-channels-store.ts:296",
|
||||||
|
"src/channels/official-channels-store.ts:299",
|
||||||
|
"src/cliproxy/accounts/drain-order.ts:131",
|
||||||
|
"src/cliproxy/accounts/drain-order.ts:139",
|
||||||
|
"src/cliproxy/accounts/drain-order.ts:246",
|
||||||
|
"src/cliproxy/accounts/drain-order.ts:253",
|
||||||
|
"src/cliproxy/accounts/drain-order.ts:279",
|
||||||
|
"src/cliproxy/accounts/drain-order.ts:342",
|
||||||
|
"src/cliproxy/accounts/registry.ts:521",
|
||||||
|
"src/cliproxy/accounts/registry.ts:536",
|
||||||
|
"src/cliproxy/accounts/registry.ts:544",
|
||||||
|
"src/cliproxy/accounts/registry.ts:589",
|
||||||
|
"src/cliproxy/accounts/registry.ts:597",
|
||||||
|
"src/cliproxy/accounts/registry.ts:755",
|
||||||
|
"src/cliproxy/accounts/registry.ts:770",
|
||||||
|
"src/cliproxy/ai-providers/managed-model-prefixes.ts:54",
|
||||||
|
"src/cliproxy/ai-providers/managed-model-prefixes.ts:71",
|
||||||
|
"src/cliproxy/ai-providers/managed-model-prefixes.ts:81",
|
||||||
|
"src/cliproxy/ai-providers/openai-compat-manager.ts:139",
|
||||||
|
"src/cliproxy/ai-providers/openai-compat-manager.ts:167",
|
||||||
|
"src/cliproxy/ai-providers/openai-compat-manager.ts:172",
|
||||||
|
"src/cliproxy/ai-providers/service.ts:242",
|
||||||
|
"src/cliproxy/ai-providers/service.ts:247",
|
||||||
|
"src/cliproxy/ai-providers/service.ts:254",
|
||||||
|
"src/cliproxy/ai-providers/service.ts:257",
|
||||||
|
"src/cliproxy/ai-providers/service.ts:260",
|
||||||
|
"src/cliproxy/ai-providers/service.ts:266",
|
||||||
|
"src/cliproxy/binary/installer.ts:127",
|
||||||
|
"src/cliproxy/binary/installer.ts:55",
|
||||||
|
"src/cliproxy/binary/installer.ts:74",
|
||||||
|
"src/cliproxy/binary/installer.ts:88",
|
||||||
|
"src/cliproxy/binary/lifecycle.ts:139",
|
||||||
|
"src/cliproxy/binary/platform-detector.ts:153",
|
||||||
|
"src/cliproxy/binary/platform-detector.ts:160",
|
||||||
|
"src/cliproxy/binary/verifier.ts:55",
|
||||||
|
"src/cliproxy/binary/version-checker.ts:108",
|
||||||
|
"src/cliproxy/config/base-config-loader.ts:54",
|
||||||
|
"src/cliproxy/config/base-config-loader.ts:66",
|
||||||
|
"src/cliproxy/config/base-config-loader.ts:81",
|
||||||
|
"src/cliproxy/config/base-config-loader.ts:91",
|
||||||
|
"src/cliproxy/config/env-builder.ts:1005",
|
||||||
|
"src/cliproxy/config/env-builder.ts:700",
|
||||||
|
"src/cliproxy/executor/auth-coordinator.ts:176",
|
||||||
|
"src/cliproxy/executor/auth-coordinator.ts:317",
|
||||||
|
"src/cliproxy/executor/browser-launch-setup.ts:120",
|
||||||
|
"src/cliproxy/executor/index.ts:378",
|
||||||
|
"src/cliproxy/executor/lifecycle-manager.ts:129",
|
||||||
|
"src/cliproxy/executor/lifecycle-manager.ts:162",
|
||||||
|
"src/cliproxy/executor/lifecycle-manager.ts:59",
|
||||||
|
"src/cliproxy/executor/proxy-resolver.ts:142",
|
||||||
|
"src/cliproxy/executor/proxy-resolver.ts:157",
|
||||||
|
"src/cliproxy/executor/proxy-resolver.ts:163",
|
||||||
|
"src/cliproxy/executor/proxy-resolver.ts:177",
|
||||||
|
"src/cliproxy/executor/session-bridge.ts:147",
|
||||||
|
"src/cliproxy/provider-capabilities.ts:257",
|
||||||
|
"src/cliproxy/proxy/https-tunnel-proxy.ts:61",
|
||||||
|
"src/cliproxy/routing/routing-strategy.ts:480",
|
||||||
|
"src/cliproxy/routing/routing-strategy.ts:488",
|
||||||
|
"src/cliproxy/routing/routing-strategy.ts:708",
|
||||||
|
"src/cliproxy/services/remote-auth-fetcher.ts:139",
|
||||||
|
"src/cliproxy/services/remote-auth-fetcher.ts:150",
|
||||||
|
"src/cliproxy/services/remote-auth-fetcher.ts:152",
|
||||||
|
"src/cliproxy/services/remote-auth-fetcher.ts:159",
|
||||||
|
"src/cliproxy/services/remote-auth-fetcher.ts:165",
|
||||||
|
"src/cliproxy/services/remote-auth-fetcher.ts:168",
|
||||||
|
"src/cliproxy/services/startup-lock.ts:208",
|
||||||
|
"src/cliproxy/services/variant-config-adapter.ts:72",
|
||||||
|
"src/cliproxy/services/variant-service.ts:371",
|
||||||
|
"src/cliproxy/services/variant-service.ts:446",
|
||||||
|
"src/codex-auth/codex-auth-dashboard-service.ts:108",
|
||||||
|
"src/codex-auth/codex-auth-dashboard-service.ts:84",
|
||||||
|
"src/codex-auth/codex-profile-paths.ts:17",
|
||||||
|
"src/codex-auth/codex-profile-paths.ts:26",
|
||||||
|
"src/codex-auth/codex-profile-registry.ts:204",
|
||||||
|
"src/codex-auth/codex-profile-registry.ts:232",
|
||||||
|
"src/codex-auth/codex-profile-registry.ts:277",
|
||||||
|
"src/codex-auth/codex-profile-registry.ts:29",
|
||||||
|
"src/codex-auth/codex-profile-registry.ts:295",
|
||||||
|
"src/codex-auth/codex-profile-registry.ts:305",
|
||||||
|
"src/codex-auth/codex-profile-registry.ts:317",
|
||||||
|
"src/codex-auth/codex-profile-registry.ts:320",
|
||||||
|
"src/codex-auth/codex-profile-registry.ts:34",
|
||||||
|
"src/codex-auth/codex-profile-registry.ts:351",
|
||||||
|
"src/codex-auth/codex-profile-registry.ts:37",
|
||||||
|
"src/codex-auth/codex-profile-registry.ts:52",
|
||||||
|
"src/codex-auth/codex-profile-registry.ts:65",
|
||||||
|
"src/codex-auth/codex-profile-registry.ts:71",
|
||||||
|
"src/codex-auth/codex-profile-registry.ts:75",
|
||||||
|
"src/codex-auth/codex-profile-registry.ts:78",
|
||||||
|
"src/codex-auth/codex-profile-registry.ts:81",
|
||||||
|
"src/codex-auth/codex-profile-registry.ts:86",
|
||||||
|
"src/codex-auth/codex-profile-registry.ts:93",
|
||||||
|
"src/codex-auth/codex-profile-registry.ts:96",
|
||||||
|
"src/codex-auth/commands/import-default-command.ts:134",
|
||||||
|
"src/codex-auth/commands/import-default-command.ts:146",
|
||||||
|
"src/codex-auth/commands/import-default-command.ts:167",
|
||||||
|
"src/commands/bar/install-subcommand.ts:137",
|
||||||
|
"src/commands/bar/install-subcommand.ts:141",
|
||||||
|
"src/commands/bar/install-subcommand.ts:149",
|
||||||
|
"src/commands/bar/install-subcommand.ts:172",
|
||||||
|
"src/commands/bar/install-subcommand.ts:181",
|
||||||
|
"src/commands/bar/install-subcommand.ts:187",
|
||||||
|
"src/commands/bar/install-subcommand.ts:228",
|
||||||
|
"src/commands/bar/install-subcommand.ts:249",
|
||||||
|
"src/commands/bar/install-subcommand.ts:259",
|
||||||
|
"src/commands/bar/install-subcommand.ts:271",
|
||||||
|
"src/commands/bar/install-subcommand.ts:289",
|
||||||
|
"src/commands/bar/install-subcommand.ts:316",
|
||||||
|
"src/commands/bar/launch-subcommand.ts:212",
|
||||||
|
"src/commands/config-channels-command.ts:431",
|
||||||
|
"src/commands/config-channels-command.ts:436",
|
||||||
|
"src/commands/config-channels-command.ts:447",
|
||||||
|
"src/commands/persist-command/arg-parsing.ts:31",
|
||||||
|
"src/commands/persist-command/backup-rotation.ts:226",
|
||||||
|
"src/commands/persist-command/backup-rotation.ts:244",
|
||||||
|
"src/commands/persist-command/backup-rotation.ts:249",
|
||||||
|
"src/commands/persist-command/backup-rotation.ts:86",
|
||||||
|
"src/commands/persist-command/handler.ts:166",
|
||||||
|
"src/commands/persist-command/handler.ts:38",
|
||||||
|
"src/commands/persist-command/secure-file.ts:104",
|
||||||
|
"src/commands/persist-command/secure-file.ts:120",
|
||||||
|
"src/commands/persist-command/secure-file.ts:142",
|
||||||
|
"src/commands/persist-command/secure-file.ts:172",
|
||||||
|
"src/commands/persist-command/secure-file.ts:174",
|
||||||
|
"src/commands/persist-command/secure-file.ts:182",
|
||||||
|
"src/commands/persist-command/secure-file.ts:98",
|
||||||
|
"src/commands/proxy-command.ts:89",
|
||||||
|
"src/commands/setup-command.ts:218",
|
||||||
|
"src/config/loader/io-locks.ts:205",
|
||||||
|
"src/config/loader/io-locks.ts:241",
|
||||||
|
"src/config/loader/io-locks.ts:295",
|
||||||
|
"src/config/loader/io-locks.ts:297",
|
||||||
|
"src/config/reserved-names.ts:92",
|
||||||
|
"src/config/unified-config-loader.ts:149",
|
||||||
|
"src/copilot/copilot-package-manager.ts:444",
|
||||||
|
"src/copilot/copilot-package-manager.ts:467",
|
||||||
|
"src/cursor/cursor-anthropic-translator.ts:113",
|
||||||
|
"src/cursor/cursor-anthropic-translator.ts:119",
|
||||||
|
"src/cursor/cursor-anthropic-translator.ts:129",
|
||||||
|
"src/cursor/cursor-anthropic-translator.ts:149",
|
||||||
|
"src/cursor/cursor-anthropic-translator.ts:169",
|
||||||
|
"src/cursor/cursor-anthropic-translator.ts:174",
|
||||||
|
"src/cursor/cursor-anthropic-translator.ts:197",
|
||||||
|
"src/cursor/cursor-anthropic-translator.ts:21",
|
||||||
|
"src/cursor/cursor-anthropic-translator.ts:44",
|
||||||
|
"src/cursor/cursor-anthropic-translator.ts:51",
|
||||||
|
"src/cursor/cursor-anthropic-translator.ts:94",
|
||||||
|
"src/cursor/cursor-client-policy.ts:33",
|
||||||
|
"src/cursor/cursor-client-policy.ts:81",
|
||||||
|
"src/cursor/cursor-client-policy.ts:85",
|
||||||
|
"src/cursor/cursor-daemon-entry.ts:148",
|
||||||
|
"src/cursor/cursor-daemon-entry.ts:153",
|
||||||
|
"src/cursor/cursor-daemon-entry.ts:158",
|
||||||
|
"src/cursor/cursor-daemon-entry.ts:168",
|
||||||
|
"src/cursor/cursor-executor.ts:206",
|
||||||
|
"src/cursor/cursor-protobuf.ts:50",
|
||||||
|
"src/cursor/cursor-translator.ts:189",
|
||||||
|
"src/delegation/headless-executor.ts:107",
|
||||||
|
"src/delegation/headless-executor.ts:122",
|
||||||
|
"src/delegation/headless-executor.ts:251",
|
||||||
|
"src/delegation/headless-executor.ts:654",
|
||||||
|
"src/dispatcher/cli-argument-parser.ts:310",
|
||||||
|
"src/dispatcher/cli-argument-parser.ts:314",
|
||||||
|
"src/dispatcher/cli-argument-parser.ts:324",
|
||||||
|
"src/dispatcher/cli-argument-parser.ts:328",
|
||||||
|
"src/dispatcher/flows/default-flow.ts:76",
|
||||||
|
"src/dispatcher/flows/settings-flow.ts:132",
|
||||||
|
"src/docker/docker-assets.ts:35",
|
||||||
|
"src/docker/docker-executor.ts:154",
|
||||||
|
"src/docker/docker-executor.ts:435",
|
||||||
|
"src/glmt/delta-accumulator.ts:154",
|
||||||
|
"src/glmt/delta-accumulator.ts:196",
|
||||||
|
"src/glmt/delta-accumulator.ts:216",
|
||||||
|
"src/glmt/delta-accumulator.ts:304",
|
||||||
|
"src/glmt/glmt-transformer.ts:92",
|
||||||
|
"src/glmt/sse-parser.ts:130",
|
||||||
|
"src/glmt/sse-parser.ts:73",
|
||||||
|
"src/management/instance-manager.ts:133",
|
||||||
|
"src/management/profile-context-sync-lock.ts:162",
|
||||||
|
"src/management/profile-context-sync-lock.ts:232",
|
||||||
|
"src/proxy/proxy-daemon-entry.ts:67",
|
||||||
|
"src/proxy/proxy-daemon-entry.ts:78",
|
||||||
|
"src/proxy/proxy-daemon-entry.ts:88",
|
||||||
|
"src/proxy/proxy-daemon.ts:98",
|
||||||
|
"src/proxy/transformers/request-transformer.ts:141",
|
||||||
|
"src/proxy/transformers/request-transformer.ts:182",
|
||||||
|
"src/proxy/transformers/request-transformer.ts:189",
|
||||||
|
"src/proxy/transformers/request-transformer.ts:242",
|
||||||
|
"src/proxy/transformers/request-transformer.ts:259",
|
||||||
|
"src/proxy/transformers/request-transformer.ts:278",
|
||||||
|
"src/proxy/transformers/request-transformer.ts:344",
|
||||||
|
"src/proxy/transformers/request-transformer.ts:360",
|
||||||
|
"src/proxy/transformers/request-transformer.ts:370",
|
||||||
|
"src/proxy/transformers/request-transformer.ts:390",
|
||||||
|
"src/proxy/transformers/request-transformer.ts:428",
|
||||||
|
"src/proxy/transformers/request-transformer.ts:439",
|
||||||
|
"src/proxy/transformers/request-transformer.ts:443",
|
||||||
|
"src/proxy/transformers/request-transformer.ts:459",
|
||||||
|
"src/proxy/transformers/request-transformer.ts:468",
|
||||||
|
"src/proxy/transformers/request-transformer.ts:487",
|
||||||
|
"src/proxy/transformers/request-transformer.ts:493",
|
||||||
|
"src/proxy/transformers/request-transformer.ts:528",
|
||||||
|
"src/proxy/transformers/request-transformer.ts:533",
|
||||||
|
"src/proxy/transformers/request-transformer.ts:538",
|
||||||
|
"src/proxy/transformers/request-transformer.ts:543",
|
||||||
|
"src/proxy/transformers/request-transformer.ts:561",
|
||||||
|
"src/proxy/transformers/request-transformer.ts:566",
|
||||||
|
"src/proxy/transformers/request-transformer.ts:573",
|
||||||
|
"src/proxy/transformers/request-transformer.ts:582",
|
||||||
|
"src/proxy/transformers/request-transformer.ts:633",
|
||||||
|
"src/proxy/transformers/request-transformer.ts:639",
|
||||||
|
"src/proxy/transformers/request-transformer.ts:644",
|
||||||
|
"src/proxy/transformers/request-transformer.ts:665",
|
||||||
|
"src/proxy/upstream-url.ts:28",
|
||||||
|
"src/shared/claude-extension-setup.ts:240",
|
||||||
|
"src/shared/claude-extension-setup.ts:247",
|
||||||
|
"src/shared/claude-extension-setup.ts:257",
|
||||||
|
"src/shared/claude-extension-setup.ts:317",
|
||||||
|
"src/shared/provider-preset-catalog.ts:308",
|
||||||
|
"src/shared/provider-preset-catalog.ts:311",
|
||||||
|
"src/shared/provider-preset-catalog.ts:320",
|
||||||
|
"src/shared/provider-preset-catalog.ts:323",
|
||||||
|
"src/shared/provider-preset-catalog.ts:326",
|
||||||
|
"src/shared/provider-preset-catalog.ts:331",
|
||||||
|
"src/shared/provider-preset-catalog.ts:334",
|
||||||
|
"src/shared/toml-object.ts:23",
|
||||||
|
"src/targets/codex-adapter.ts:103",
|
||||||
|
"src/targets/codex-adapter.ts:275",
|
||||||
|
"src/targets/codex-adapter.ts:319",
|
||||||
|
"src/targets/codex-adapter.ts:326",
|
||||||
|
"src/targets/codex-adapter.ts:363",
|
||||||
|
"src/targets/codex-cliproxy-provider-config.ts:138",
|
||||||
|
"src/targets/codex-cliproxy-provider-config.ts:141",
|
||||||
|
"src/targets/codex-cliproxy-provider-config.ts:151",
|
||||||
|
"src/targets/codex-cliproxy-provider-config.ts:177",
|
||||||
|
"src/targets/droid-adapter.ts:32",
|
||||||
|
"src/targets/droid-adapter.ts:35",
|
||||||
|
"src/targets/droid-adapter.ts:68",
|
||||||
|
"src/targets/droid-adapter.ts:74",
|
||||||
|
"src/targets/droid-config-manager.ts:335",
|
||||||
|
"src/targets/droid-config-manager.ts:34",
|
||||||
|
"src/targets/droid-config-manager.ts:354",
|
||||||
|
"src/targets/droid-config-manager.ts:357",
|
||||||
|
"src/targets/droid-config-manager.ts:390",
|
||||||
|
"src/targets/droid-config-manager.ts:421",
|
||||||
|
"src/targets/droid-config-manager.ts:429",
|
||||||
|
"src/targets/droid-config-manager.ts:449",
|
||||||
|
"src/targets/target-registry.ts:27",
|
||||||
|
"src/targets/target-resolver.ts:137",
|
||||||
|
"src/targets/target-resolver.ts:161",
|
||||||
|
"src/targets/target-resolver.ts:171",
|
||||||
|
"src/utils/browser/browser-policy.ts:35",
|
||||||
|
"src/utils/browser/browser-policy.ts:43",
|
||||||
|
"src/utils/browser/chrome-reuse.ts:114",
|
||||||
|
"src/utils/browser/chrome-reuse.ts:119",
|
||||||
|
"src/utils/browser/chrome-reuse.ts:142",
|
||||||
|
"src/utils/browser/chrome-reuse.ts:155",
|
||||||
|
"src/utils/browser/chrome-reuse.ts:160",
|
||||||
|
"src/utils/browser/chrome-reuse.ts:165",
|
||||||
|
"src/utils/browser/chrome-reuse.ts:41",
|
||||||
|
"src/utils/browser/chrome-reuse.ts:64",
|
||||||
|
"src/utils/browser/chrome-reuse.ts:80",
|
||||||
|
"src/utils/browser/chrome-reuse.ts:84",
|
||||||
|
"src/utils/browser/chrome-reuse.ts:95",
|
||||||
|
"src/utils/browser/mcp-installer.ts:342",
|
||||||
|
"src/utils/claude-spawner.ts:46",
|
||||||
|
"src/utils/config-manager.ts:298",
|
||||||
|
"src/utils/config-manager.ts:302",
|
||||||
|
"src/utils/prompt.ts:112",
|
||||||
|
"src/utils/prompt.ts:157",
|
||||||
|
"src/utils/prompt.ts:54",
|
||||||
|
"src/utils/proxy-env.ts:36",
|
||||||
|
"src/utils/proxy-env.ts:40",
|
||||||
|
"src/utils/retry-strategy.ts:96",
|
||||||
|
"src/utils/retry-strategy.ts:99",
|
||||||
|
"src/utils/shell-completion.ts:101",
|
||||||
|
"src/utils/shell-completion.ts:143",
|
||||||
|
"src/utils/shell-completion.ts:191",
|
||||||
|
"src/utils/shell-completion.ts:224",
|
||||||
|
"src/utils/shell-completion.ts:269",
|
||||||
|
"src/utils/shell-completion.ts:288",
|
||||||
|
"src/utils/shell-completion.ts:74",
|
||||||
|
"src/utils/websearch/mcp-installer.ts:438",
|
||||||
|
"src/utils/websearch/profile-hook-injector.ts:206",
|
||||||
|
"src/web-server/index.ts:266",
|
||||||
|
"src/web-server/services/claude-extension-binding-service.ts:144",
|
||||||
|
"src/web-server/services/claude-extension-binding-service.ts:185",
|
||||||
|
"src/web-server/services/claude-extension-binding-service.ts:195",
|
||||||
|
"src/web-server/services/claude-extension-binding-service.ts:207",
|
||||||
|
"src/web-server/services/claude-extension-binding-service.ts:255",
|
||||||
|
"src/web-server/services/claude-extension-binding-service.ts:273",
|
||||||
|
"src/web-server/services/claude-extension-binding-service.ts:82",
|
||||||
|
"src/web-server/services/claude-extension-binding-service.ts:83",
|
||||||
|
"src/web-server/services/claude-extension-binding-service.ts:84",
|
||||||
|
"src/web-server/services/claude-extension-binding-service.ts:89",
|
||||||
|
"src/web-server/services/claude-extension-settings-service.ts:186",
|
||||||
|
"src/web-server/services/claude-extension-settings-service.ts:190",
|
||||||
|
"src/web-server/services/claude-extension-settings-service.ts:202",
|
||||||
|
"src/web-server/services/claude-extension-settings-service.ts:226",
|
||||||
|
"src/web-server/services/compatible-cli-docs-registry.ts:171",
|
||||||
|
"src/web-server/services/compatible-cli-json-file-service.ts:171",
|
||||||
|
"src/web-server/services/compatible-cli-json-file-service.ts:174",
|
||||||
|
"src/web-server/services/compatible-cli-json-file-service.ts:191",
|
||||||
|
"src/web-server/services/compatible-cli-json-file-service.ts:194",
|
||||||
|
"src/web-server/services/compatible-cli-json-file-service.ts:203",
|
||||||
|
"src/web-server/services/compatible-cli-json-file-service.ts:206",
|
||||||
|
"src/web-server/services/compatible-cli-toml-file-service.ts:124",
|
||||||
|
"src/web-server/services/compatible-cli-toml-file-service.ts:127",
|
||||||
|
"src/web-server/services/compatible-cli-toml-file-service.ts:292",
|
||||||
|
"src/web-server/services/compatible-cli-toml-file-service.ts:295",
|
||||||
|
"src/web-server/services/compatible-cli-toml-file-service.ts:89",
|
||||||
|
"src/web-server/services/compatible-cli-toml-file-service.ts:92",
|
||||||
|
"src/web-server/usage/cliproxy-usage-syncer.ts:302",
|
||||||
|
"src/web-server/usage/handlers.ts:101",
|
||||||
|
"src/web-server/usage/handlers.ts:66",
|
||||||
|
"src/web-server/usage/handlers.ts:73",
|
||||||
|
"src/web-server/usage/handlers.ts:74",
|
||||||
|
"src/web-server/usage/handlers.ts:75",
|
||||||
|
"src/web-server/usage/handlers.ts:84",
|
||||||
|
"src/web-server/usage/handlers.ts:93",
|
||||||
|
"src/web-server/usage/profile-filter.ts:15",
|
||||||
|
"src/web-server/usage/profile-filter.ts:21",
|
||||||
|
"src/web-server/usage/sqlite-cli.ts:101",
|
||||||
|
"src/web-server/usage/sqlite-cli.ts:138",
|
||||||
|
"src/web-server/usage/sqlite-cli.ts:159",
|
||||||
|
"src/web-server/usage/sqlite-cli.ts:175",
|
||||||
|
"src/web-server/usage/sqlite-cli.ts:179",
|
||||||
|
"src/web-server/usage/sqlite-cli.ts:77",
|
||||||
|
"src/web-server/usage/sqlite-cli.ts:90"
|
||||||
|
]
|
||||||
@@ -1,6 +1,14 @@
|
|||||||
import tseslint from '@typescript-eslint/eslint-plugin';
|
import tseslint from '@typescript-eslint/eslint-plugin';
|
||||||
import tsparser from '@typescript-eslint/parser';
|
import tsparser from '@typescript-eslint/parser';
|
||||||
import prettier from 'eslint-config-prettier';
|
import prettier from 'eslint-config-prettier';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import noNewThrowError from './eslint-rules/no-new-throw-error.js';
|
||||||
|
|
||||||
|
const __configDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const baselinePath = path.join(__configDir, 'eslint-rules', 'throw-error-baseline.json');
|
||||||
|
const throwErrorBaseline = JSON.parse(fs.readFileSync(baselinePath, 'utf8'));
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
@@ -16,6 +24,8 @@ export default [
|
|||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
'@typescript-eslint': tseslint,
|
'@typescript-eslint': tseslint,
|
||||||
|
// Local enforcement rules (P7 gates).
|
||||||
|
ccs: { rules: { 'no-new-throw-error': noNewThrowError } },
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
// TypeScript rules - upgraded to errors for stricter type safety
|
// TypeScript rules - upgraded to errors for stricter type safety
|
||||||
@@ -32,6 +42,15 @@ export default [
|
|||||||
'prefer-const': 'error',
|
'prefer-const': 'error',
|
||||||
'no-var': 'error',
|
'no-var': 'error',
|
||||||
'eqeqeq': ['error', 'always'],
|
'eqeqeq': ['error', 'always'],
|
||||||
|
|
||||||
|
// P7 enforcement gates:
|
||||||
|
// - no-new-throw-error: error on NEW throw new Error(...) outside the
|
||||||
|
// generated baseline (eslint-rules/throw-error-baseline.json). Forces
|
||||||
|
// the typed-error taxonomy (src/errors/error-types.ts). Regenerate the
|
||||||
|
// baseline with: node scripts/generate-throw-error-baseline.js
|
||||||
|
// - max-lines: warn on files over 400 LOC (goal of P5/P6 god-file splits).
|
||||||
|
'ccs/no-new-throw-error': ['error', { allowlist: throwErrorBaseline }],
|
||||||
|
'max-lines': ['warn', { max: 400, skipBlankLines: true, skipComments: true }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the baseline allowlist for eslint-rules/no-new-throw-error.js.
|
||||||
|
*
|
||||||
|
* Walks src/ (non-test), finds every `throw new Error(...)` site using the same
|
||||||
|
* comment-stripping as scripts/maintainability-metrics.js (so the baseline
|
||||||
|
* matches what the ESLint AST sees — comments are not ThrowStatement nodes),
|
||||||
|
* and writes eslint-rules/throw-error-baseline.json as a sorted array of
|
||||||
|
* `${relativePath}:${line}` keys.
|
||||||
|
*
|
||||||
|
* Run after intentionally adding a new grandfathered throw, or quarterly to
|
||||||
|
* prune entries that have since been converted to typed errors.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const ROOT_DIR = path.resolve(__dirname, '..');
|
||||||
|
const SRC_DIR = path.join(ROOT_DIR, 'src');
|
||||||
|
const OUTPUT_PATH = path.join(ROOT_DIR, 'eslint-rules', 'throw-error-baseline.json');
|
||||||
|
|
||||||
|
const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
|
||||||
|
const THROW_NEW_ERROR_REGEX = /\bthrow\s+new\s+Error\s*\(/g;
|
||||||
|
|
||||||
|
function isTestPath(relPath) {
|
||||||
|
return (
|
||||||
|
/(?:^|\/)(?:__tests__|tests?)\//.test(relPath) ||
|
||||||
|
/\.test\./.test(relPath) ||
|
||||||
|
/\.spec\./.test(relPath)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPosix(p) {
|
||||||
|
return p.split(path.sep).join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
function relPath(fullPath) {
|
||||||
|
return toPosix(path.relative(ROOT_DIR, fullPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lazy require: hardening-inventory.js requires this module's sibling maintainability-metrics.js
|
||||||
|
// at top level; requiring it back at top level here would capture a partial module.exports.
|
||||||
|
// A function-scope require resolves against the fully-loaded module at call time.
|
||||||
|
function stripCommentsOnce(sourceText) {
|
||||||
|
return require('./hardening-inventory.js').stripComments(sourceText);
|
||||||
|
}
|
||||||
|
|
||||||
|
function walkFiles(dirPath) {
|
||||||
|
const out = [];
|
||||||
|
let entries;
|
||||||
|
try {
|
||||||
|
entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
||||||
|
} catch {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.name === 'node_modules' || entry.name === 'dist') continue;
|
||||||
|
const full = path.join(dirPath, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
out.push.apply(out, walkFiles(full));
|
||||||
|
} else if (entry.isFile() && SOURCE_EXTENSIONS.has(path.extname(full))) {
|
||||||
|
out.push(full);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectSites() {
|
||||||
|
const sites = [];
|
||||||
|
for (const full of walkFiles(SRC_DIR)) {
|
||||||
|
const rel = relPath(full);
|
||||||
|
if (isTestPath(rel)) continue;
|
||||||
|
// Match on the RAW source (not comment-stripped). ESLint lints the raw
|
||||||
|
// file, so the baseline must reflect real throw lines as the AST sees them.
|
||||||
|
// stripComments can undercount on files whose regex/template literals confuse
|
||||||
|
// its state machine; raw matching is a superset (may include comment/string
|
||||||
|
// mentions, which are harmless unused allowlist entries) and never undercounts.
|
||||||
|
const sourceText = fs.readFileSync(full, 'utf8');
|
||||||
|
const re = new RegExp(THROW_NEW_ERROR_REGEX.source, 'g');
|
||||||
|
let match;
|
||||||
|
while ((match = re.exec(sourceText)) !== null) {
|
||||||
|
const line = sourceText.slice(0, match.index).split(/\r?\n/).length;
|
||||||
|
sites.push(`${rel}:${line}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sites.sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const sites = collectSites();
|
||||||
|
fs.mkdirSync(path.dirname(OUTPUT_PATH), { recursive: true });
|
||||||
|
fs.writeFileSync(OUTPUT_PATH, JSON.stringify(sites, null, 2) + '\n', 'utf8');
|
||||||
|
console.log(`[throw-error-baseline] ${sites.length} sites -> ${relPath(OUTPUT_PATH)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
main();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { collectSites, OUTPUT_PATH };
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { describe, expect, test } from 'bun:test';
|
||||||
|
|
||||||
|
const rule = require('../../../eslint-rules/no-new-throw-error.js');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* P7 rule test: no-new-throw-error flags NEW throw new Error(...) sites not in
|
||||||
|
* the baseline allowlist, while grandfathered sites, typed throws, and
|
||||||
|
* re-throws pass.
|
||||||
|
*
|
||||||
|
* Exercises the rule's create() directly with a mock context and AST node so
|
||||||
|
* the test does not depend on the ESLint runtime under bun. The live
|
||||||
|
* `bun run lint` already proves the ESLint integration end-to-end (0 violations
|
||||||
|
* against the generated baseline).
|
||||||
|
*/
|
||||||
|
describe('ccs/no-new-throw-error rule logic', () => {
|
||||||
|
const FILENAME = 'src/sample/fixture.ts';
|
||||||
|
|
||||||
|
function node(argument: unknown, line: number) {
|
||||||
|
return {
|
||||||
|
type: 'ThrowStatement',
|
||||||
|
loc: { start: { line, column: 0 } },
|
||||||
|
argument,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function runRule(throwNode: unknown, allowlist: string[]): Array<{ messageId: string }> {
|
||||||
|
const reports: Array<{ messageId: string }> = [];
|
||||||
|
const context = {
|
||||||
|
options: [{ allowlist }],
|
||||||
|
getFilename: () => FILENAME,
|
||||||
|
report: (r: { messageId: string }) => reports.push(r),
|
||||||
|
};
|
||||||
|
const visitors = rule.create(context);
|
||||||
|
visitors.ThrowStatement(throwNode);
|
||||||
|
return reports;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newErrorArg = { type: 'NewExpression', callee: { type: 'Identifier', name: 'Error' } };
|
||||||
|
const newTypedArg = { type: 'NewExpression', callee: { type: 'Identifier', name: 'ConfigError' } };
|
||||||
|
const rethrowArg = { type: 'Identifier', name: 'err' };
|
||||||
|
|
||||||
|
test('flags a throw new Error off the allowlist', () => {
|
||||||
|
expect(runRule(node(newErrorArg, 1), []).some((r) => r.messageId === 'unexpected')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('passes a throw new Error that is on the allowlist (file:line match)', () => {
|
||||||
|
expect(runRule(node(newErrorArg, 1), [`${FILENAME}:1`]).some((r) => r.messageId === 'unexpected')).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('flags when the throw line differs from the allowlisted line', () => {
|
||||||
|
expect(runRule(node(newErrorArg, 2), [`${FILENAME}:1`]).some((r) => r.messageId === 'unexpected')).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('passes typed throws (ConfigError / AuthError)', () => {
|
||||||
|
const typed = { type: 'NewExpression', callee: { type: 'Identifier', name: 'AuthError' } };
|
||||||
|
expect(runRule(node(typed, 1), []).some((r) => r.messageId === 'unexpected')).toBe(false);
|
||||||
|
expect(runRule(node(newTypedArg, 1), []).some((r) => r.messageId === 'unexpected')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('passes re-throws and non-NewExpression throws', () => {
|
||||||
|
expect(runRule(node(rethrowArg, 1), []).some((r) => r.messageId === 'unexpected')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user