Skip to content

Commit 4322e38

Browse files
committed
fix(audits): drop the OTel exemption for rebound names, catch optional .with calls and defaulted partialize params
1 parent adcf94f commit 4322e38

7 files changed

Lines changed: 47 additions & 7 deletions

File tree

‎.agents/skills/add-block/SKILL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ When several fields are mutually exclusive alternatives, mark them all `required
310310
other paths ever get a chance to supply the value.
311311

312312
**Constraints (block-wide):**
313-
- `canonicalParamId` must not equal the `id` of a subblock that has no `canonicalParamId`. A group member may share it, as `channel` does in the canonicalParamId Pattern below.
313+
- `canonicalParamId` may equal only the `id` of a member of its own group, as `channel` does in the canonicalParamId Pattern below; it must never equal any other subblock's `id`. (`blocks.test.ts` enforces the case of a subblock with no `canonicalParamId`.)
314314
- One canonical id links exactly one basic/advanced pair for one logical parameter. Groups are keyed by canonical id across every subblock and hold one `basicId`, so two operations that each need a pair need two canonical ids.
315315
- All members of a group share the same `required` status.
316316

‎.claude/rules/sim-integrations.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ The full authoring instructions — tool/block/icon/trigger scaffolding, SubBloc
1717
- Tool IDs and the two registration/coercion rules are in the root `CLAUDE.md` → Integrations. `blocks/registry.ts` holds only the accessor functions; triggers register in `triggers/registry.ts`.
1818
- Give every subblock a unique `id`: duplicates collide silently (the last definition wins). `blocks.test.ts` fails a duplicate within one condition unless the copies are a basic/advanced mode-swap pair, one basic plus trigger-mode copies, or all carry `canonicalParamId`. The only sanctioned cross-condition reuse is the hosted-key `apiKey` pair (`/add-hosted-key`), where both fields deliberately share one value.
1919
- Keep block outputs aligned with what the referenced tools actually return, and block `tools.access` aligned with the registered tool IDs.
20-
- `canonicalParamId` must NOT match the `id` of a subblock that has no `canonicalParamId` (a group member may share it, as the `add-block` skill's `channel` example does), must be unique **block-wide** (groups are keyed by canonical id across every subblock and hold exactly one `basicId`, so two operations that each need a pair need two different canonical ids), and all subblocks in a canonical group must share the same `required` status. The `inputs` section and the params function reference canonical IDs, not raw subblock IDs — the serializer deletes the subblock IDs and republishes the active member's value under the canonical ID.
20+
- `canonicalParamId` may match only the `id` of a member of its own group (as the `add-block` skill's `channel` example does), never any other subblock's `id`, must be unique **block-wide** (groups are keyed by canonical id across every subblock and hold exactly one `basicId`, so two operations that each need a pair need two different canonical ids), and all subblocks in a canonical group must share the same `required` status. The `inputs` section and the params function reference canonical IDs, not raw subblock IDs — the serializer deletes the subblock IDs and republishes the active member's value under the canonical ID.
2121
- A canonical pair carries ONE concept. For files that is upload (basic) + file reference (advanced), normalized with `normalizeFileInput`, as in Gmail attachments (`blocks/blocks/gmail.ts`). Never overload the advanced side with alternate identifiers (URL, provider asset ID) — give those their own subblocks, mark mutually exclusive sources `required: false`, and enforce "exactly one" at execution.
2222
- A sub-block's option list is EITHER `selectorKey` (a registered selector — the only way to load a remote list, and the only one that works off the canvas) OR `options` (a static array, or a pure function of the block's own values). Never fetch from a block definition, and never read the workflow stores there. A credential sub-block needs `canonicalParamId: 'oauthCredential'` for its dependants' selectors to resolve. A secret must never appear in a selector's `getQueryKey`. `bun run check:fork-dependent-coverage` fails a `dependsOn` under a credential/KB/table anchor that the fork sync modal cannot offer.
2323
- Integration blocks (`category: 'tools'`) must set `integrationType` (`integration-catalog:check` fails without it) and export a `{Service}BlockMeta` (with `tags`); set `authMode` and `docsLink` too, which otherwise fall back to a credential-subblock guess and the generated docs page — see the `/add-block` skill's BlockMeta section. `{Service}BlockMeta.skills` must be grounded in operations the block exposes via `tools.access` and sourced from real, popular use cases found online — never invented.

‎.claude/rules/sim-react-performance.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ return items.sort(compare)
7777
return [...items].sort(compare)
7878
```
7979

80-
**Do NOT use `toSorted()` / `toReversed()` / `with()` / `toSpliced()`.** They are ES2023 *runtime* methods — and a tsconfig `"lib": ["ES2023"]` only makes them **type-check**, it does not make them **run**. Next/SWC compiles syntax but does **not** polyfill prototype methods, and the default browserslist still includes browsers without them (`toSorted` landed in Safari 16 / iOS 16, so any device capped at iOS 15 throws `TypeError: x.toSorted is not a function` and crashes the page). The perf difference vs `[...arr].sort()` is negligible (both allocate one array), so the copy-then-sort form is used everywhere: whether a module reaches the browser is not visible from its path. `check:utils` flags `toSorted`/`toReversed`/`toSpliced` repo-wide and every two-argument `.with(index, value)` call on any receiver except OpenTelemetry's context API (the `context` export of `@opentelemetry/api`, under any alias or via a namespace import); a deliberate exception carries `// utils-lint-allow: <reason>`.
80+
**Do NOT use `toSorted()` / `toReversed()` / `with()` / `toSpliced()`.** They are ES2023 *runtime* methods — and a tsconfig `"lib": ["ES2023"]` only makes them **type-check**, it does not make them **run**. Next/SWC compiles syntax but does **not** polyfill prototype methods, and the default browserslist still includes browsers without them (`toSorted` landed in Safari 16 / iOS 16, so any device capped at iOS 15 throws `TypeError: x.toSorted is not a function` and crashes the page). The perf difference vs `[...arr].sort()` is negligible (both allocate one array), so the copy-then-sort form is used everywhere: whether a module reaches the browser is not visible from its path. `check:utils` flags `toSorted`/`toReversed`/`toSpliced` repo-wide and every two-argument `.with(index, value)` call on any receiver except OpenTelemetry's context API (the `context` export of `@opentelemetry/api`, under any alias or via a namespace import, when the file does not redeclare that name); a deliberate exception carries `// utils-lint-allow: <reason>`.
8181

8282
## Run independent awaits in parallel
8383

‎.cursor/rules/sim-integrations.mdc‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ The full authoring instructions — tool/block/icon/trigger scaffolding, SubBloc
1616
- Tool IDs and the two registration/coercion rules are in the root `CLAUDE.md` → Integrations. `blocks/registry.ts` holds only the accessor functions; triggers register in `triggers/registry.ts`.
1717
- Give every subblock a unique `id`: duplicates collide silently (the last definition wins). `blocks.test.ts` fails a duplicate within one condition unless the copies are a basic/advanced mode-swap pair, one basic plus trigger-mode copies, or all carry `canonicalParamId`. The only sanctioned cross-condition reuse is the hosted-key `apiKey` pair (`/add-hosted-key`), where both fields deliberately share one value.
1818
- Keep block outputs aligned with what the referenced tools actually return, and block `tools.access` aligned with the registered tool IDs.
19-
- `canonicalParamId` must NOT match the `id` of a subblock that has no `canonicalParamId` (a group member may share it, as the `add-block` skill's `channel` example does), must be unique **block-wide** (groups are keyed by canonical id across every subblock and hold exactly one `basicId`, so two operations that each need a pair need two different canonical ids), and all subblocks in a canonical group must share the same `required` status. The `inputs` section and the params function reference canonical IDs, not raw subblock IDs — the serializer deletes the subblock IDs and republishes the active member's value under the canonical ID.
19+
- `canonicalParamId` may match only the `id` of a member of its own group (as the `add-block` skill's `channel` example does), never any other subblock's `id`, must be unique **block-wide** (groups are keyed by canonical id across every subblock and hold exactly one `basicId`, so two operations that each need a pair need two different canonical ids), and all subblocks in a canonical group must share the same `required` status. The `inputs` section and the params function reference canonical IDs, not raw subblock IDs — the serializer deletes the subblock IDs and republishes the active member's value under the canonical ID.
2020
- A canonical pair carries ONE concept. For files that is upload (basic) + file reference (advanced), normalized with `normalizeFileInput`, as in Gmail attachments (`blocks/blocks/gmail.ts`). Never overload the advanced side with alternate identifiers (URL, provider asset ID) — give those their own subblocks, mark mutually exclusive sources `required: false`, and enforce "exactly one" at execution.
2121
- A sub-block's option list is EITHER `selectorKey` (a registered selector — the only way to load a remote list, and the only one that works off the canvas) OR `options` (a static array, or a pure function of the block's own values). Never fetch from a block definition, and never read the workflow stores there. A credential sub-block needs `canonicalParamId: 'oauthCredential'` for its dependants' selectors to resolve. A secret must never appear in a selector's `getQueryKey`. `bun run check:fork-dependent-coverage` fails a `dependsOn` under a credential/KB/table anchor that the fork sync modal cannot offer.
2222
- Integration blocks (`category: 'tools'`) must set `integrationType` (`integration-catalog:check` fails without it) and export a `{Service}BlockMeta` (with `tags`); set `authMode` and `docsLink` too, which otherwise fall back to a credential-subblock guess and the generated docs page — see the `/add-block` skill's BlockMeta section. `{Service}BlockMeta.skills` must be grounded in operations the block exposes via `tools.access` and sourced from real, popular use cases found online — never invented.

‎.cursor/rules/sim-react-performance.mdc‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ return items.sort(compare)
8080
return [...items].sort(compare)
8181
```
8282

83-
**Do NOT use `toSorted()` / `toReversed()` / `with()` / `toSpliced()`.** They are ES2023 *runtime* methods — and a tsconfig `"lib": ["ES2023"]` only makes them **type-check**, it does not make them **run**. Next/SWC compiles syntax but does **not** polyfill prototype methods, and the default browserslist still includes browsers without them (`toSorted` landed in Safari 16 / iOS 16, so any device capped at iOS 15 throws `TypeError: x.toSorted is not a function` and crashes the page). The perf difference vs `[...arr].sort()` is negligible (both allocate one array), so the copy-then-sort form is used everywhere: whether a module reaches the browser is not visible from its path. `check:utils` flags `toSorted`/`toReversed`/`toSpliced` repo-wide and every two-argument `.with(index, value)` call on any receiver except OpenTelemetry's context API (the `context` export of `@opentelemetry/api`, under any alias or via a namespace import); a deliberate exception carries `// utils-lint-allow: <reason>`.
83+
**Do NOT use `toSorted()` / `toReversed()` / `with()` / `toSpliced()`.** They are ES2023 *runtime* methods — and a tsconfig `"lib": ["ES2023"]` only makes them **type-check**, it does not make them **run**. Next/SWC compiles syntax but does **not** polyfill prototype methods, and the default browserslist still includes browsers without them (`toSorted` landed in Safari 16 / iOS 16, so any device capped at iOS 15 throws `TypeError: x.toSorted is not a function` and crashes the page). The perf difference vs `[...arr].sort()` is negligible (both allocate one array), so the copy-then-sort form is used everywhere: whether a module reaches the browser is not visible from its path. `check:utils` flags `toSorted`/`toReversed`/`toSpliced` repo-wide and every two-argument `.with(index, value)` call on any receiver except OpenTelemetry's context API (the `context` export of `@opentelemetry/api`, under any alias or via a namespace import, when the file does not redeclare that name); a deliberate exception carries `// utils-lint-allow: <reason>`.
8484

8585
## Run independent awaits in parallel
8686

‎scripts/check-utils-enforcement.ts‎

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ const BANNED_PATTERNS: Array<{
174174
]
175175

176176
/** Cheap gate: only files that contain a `.with(` call are parsed. */
177-
const WITH_CALL = /\.with\s*\(/
177+
const WITH_CALL = /\.with\s*(?:\?\.\s*)?\(/
178178

179179
/** A Babel AST node, read structurally rather than through `@babel/types`. */
180180
interface SyntaxNode extends Record<string, unknown> {
@@ -228,6 +228,38 @@ function otelContextBindings(program: SyntaxNode): {
228228
return { contexts, namespaces }
229229
}
230230

231+
/** Names bound by a pattern: `a`, `{ a, b: c }`, `[a, ...rest]`, `a = 1`. */
232+
function patternNames(pattern: unknown, names: string[]): void {
233+
if (!isSyntaxNode(pattern)) return
234+
if (pattern.type === 'Identifier' && typeof pattern.name === 'string') names.push(pattern.name)
235+
else if (pattern.type === 'AssignmentPattern') patternNames(pattern.left, names)
236+
else if (pattern.type === 'RestElement') patternNames(pattern.argument, names)
237+
else if (pattern.type === 'ArrayPattern' && Array.isArray(pattern.elements)) {
238+
for (const element of pattern.elements) patternNames(element, names)
239+
} else if (pattern.type === 'ObjectPattern' && Array.isArray(pattern.properties)) {
240+
for (const property of pattern.properties) {
241+
patternNames(
242+
isSyntaxNode(property) && property.type === 'ObjectProperty' ? property.value : property,
243+
names
244+
)
245+
}
246+
}
247+
}
248+
249+
/** Every name a variable, parameter, catch clause, function, or class declares in the file. */
250+
function declaredNames(program: SyntaxNode): Set<string> {
251+
const names: string[] = []
252+
walkNodes(program, (node) => {
253+
if (node.type === 'VariableDeclarator') patternNames(node.id, names)
254+
else if (node.type === 'CatchClause') patternNames(node.param, names)
255+
else if (/Function|ObjectMethod|ClassMethod/.test(node.type)) {
256+
if (Array.isArray(node.params)) for (const param of node.params) patternNames(param, names)
257+
if (node.type === 'FunctionDeclaration') patternNames(node.id, names)
258+
} else if (node.type === 'ClassDeclaration') patternNames(node.id, names)
259+
})
260+
return new Set(names)
261+
}
262+
231263
/** Whether `receiver` is OpenTelemetry's context object: `context`, an alias, or `api.context`. */
232264
function isOtelContext(
233265
receiver: unknown,
@@ -265,6 +297,11 @@ function findArrayWithCalls(file: string, content: string): number[] {
265297
if (!isSyntaxNode(program)) return []
266298

267299
const bindings = otelContextBindings(program)
300+
// A name the file also declares elsewhere may be shadowed at the call; exempt only unique bindings.
301+
for (const name of declaredNames(program)) {
302+
bindings.contexts.delete(name)
303+
bindings.namespaces.delete(name)
304+
}
268305
const offsets: number[] = []
269306
walkNodes(program, (node) => {
270307
if (node.type !== 'CallExpression' && node.type !== 'OptionalCallExpression') return

‎scripts/check-zustand-v5-selectors.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,10 @@ function returnedExpressions(fn: SyntaxNode): unknown[] {
372372
* The parameter binding that holds every state field: `s` in `(s) => …`, or `rest` in
373373
* `({ a, ...rest }) => …`, which holds every field but the ones named (a deny-list).
374374
*/
375-
function wholeStateBinding(param: unknown): { name: string; reason: string } | null {
375+
function wholeStateBinding(rawParam: unknown): { name: string; reason: string } | null {
376+
// A default (`(state = {} as State) => …`) binds the same value.
377+
const param =
378+
isSyntaxNode(rawParam) && rawParam.type === 'AssignmentPattern' ? rawParam.left : rawParam
376379
if (!isSyntaxNode(param)) return null
377380
if (param.type === 'Identifier' && typeof param.name === 'string') {
378381
return { name: param.name, reason: 'persist partialize spreads the whole state' }

0 commit comments

Comments
 (0)