Skip to content

feat(compiler): SYN020/SYN021 — warn on Date/performance ambient time access that bypasses the capability model (?bs 0.7+)#176

Open
marcelofarias wants to merge 5 commits into
mainfrom
botkowski/syn020-021-rebase
Open

feat(compiler): SYN020/SYN021 — warn on Date/performance ambient time access that bypasses the capability model (?bs 0.7+)#176
marcelofarias wants to merge 5 commits into
mainfrom
botkowski/syn020-021-rebase

Conversation

@marcelofarias

Copy link
Copy Markdown
Owner

Summary

  • Adds SYN020 to the compiler syn-check pass: fires when a fn body calls Date.now(), Date?.now(), Date.now?.(), constructs new Date() / new Date<T>() (no-arg, ambient time), or calls Date() / Date?.() in ?bs 0.7+.
  • Adds SYN021 to the compiler syn-check pass: fires when a fn body calls performance.now(), performance?.now(), performance.now?.(), or reads performance.timeOrigin / performance?.timeOrigin in ?bs 0.7+.
  • Both diagnostics flag ambient time injection that is invisible to botscript's capability model: uses { time } covers time.* stdlib calls, not the Date or performance globals. A fn that reads these has an undeclared time dependency — callers cannot see it and tests cannot control the clock value observed.
  • Wired through: error-codes registry, MCP explain output, KNOWN_CODES contract test, AGENTS.md diagnostic table, README.md explain-tool code list.

This is a clean rebase of PR #169 onto current main (which added SYN019, SYN022, SYN023 since the original branch was opened). SYN020 and SYN021 are inserted in numeric order after SYN019 and before SYN022/SYN023. All 1499 tests pass.

What botscript form triggers SYN020

?bs 0.7
fn isExpired(expiresAt: number) -> bool {
  return Date.now() > expiresAt  // SYN020
}

fn ts() -> string {
  return new Date()  // SYN020
}

fn ts2() -> string {
  return Date?.()    // SYN020
}

What botscript form triggers SYN021

?bs 0.7
fn elapsed(start: number) -> number {
  return performance.now() - start  // SYN021
}

Fix

// preferred — pass nowMs as a parameter; tests can control it
fn isExpired(nowMs: number, expiresAt: number) -> bool {
  return nowMs > expiresAt
}

// escape hatch when ambient read is necessary at an entry point
fn elapsed(start: number) -> number {
  return unsafe "uses performance.now for timing" { performance.now() } - start
}

… SYN021 (performance.now/timeOrigin time bypass) rebased onto main

Ports SYN020 and SYN021 detection from botkowski/syn021-performance-time, inserting them in numeric order after SYN019 and before SYN022/SYN023 which were added to main since the original branch was opened.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds two new compiler syn-check warnings (SYN020/SYN021) to detect ambient time reads via Date and performance APIs that bypass botscript’s uses { time } capability model, and wires the new diagnostics through the compiler + MCP explanation surfaces with tests.

Changes:

  • Add SYN020 detection for Date.now(), new Date() / new Date (no parens), and Date() / Date?.() ambient-time reads in ?bs 0.7+.
  • Add SYN021 detection for performance.now() calls and performance.timeOrigin reads in ?bs 0.7+.
  • Register both codes in error-code registries/MCP explanations and add dedicated compiler tests + MCP KNOWN_CODES coverage.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/compiler/src/passes/syn-check.ts Implements SYN020/SYN021 token-pattern detection with unsafe {} / unsafe fn suppression and version gating.
packages/compiler/src/error-codes.ts Registers SYN020/SYN021 (rule/idiom/rewrite/example) in the compiler error-code registry.
packages/mcp/src/explanations.ts Adds long-form MCP explain entries for SYN020 and SYN021, including detected forms and suggested fixes.
packages/mcp/tests/server.test.ts Extends MCP KNOWN_CODES allowlist to include SYN020 and SYN021.
packages/compiler/tests/error-codes.test.ts Updates compiler registry allowlist to include SYN020 and SYN021.
packages/compiler/tests/syn020-check.test.ts New tests covering SYN020 positive/negative cases, version gating, and unsafe suppression.
packages/compiler/tests/syn021-check.test.ts New tests covering SYN021 positive/negative cases, version gating, and unsafe suppression.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +140 to +144
* SYN020 A `Date.now()`, `new Date()`, `new Date` (no parens), or `Date()` call was detected
* in a fn body (?bs 0.7+). These forms inject the current time at runtime but are invisible
* to botscript's capability model: `uses { time }` covers `time.*` stdlib calls, not the
* `Date` global. A fn that calls these forms has an undeclared time dependency — callers
* cannot see it and tests cannot control the time value observed by the fn.
…ain list

Addresses Copilot review comment on PR #176: AGENTS.md and README.md were
missing SYN020 (Date ambient time) and SYN021 (performance timing) entries
between SYN019 and SYN022.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Comment on lines +1681 to +1683
const hasDateArgs20 = !hasNew20 && firstInsideIdx20 !== callTok20.matchedAt;
const callForm20 = hasNew20 ? "new Date()" : isOpt20 ? "Date?.()" : hasDateArgs20 ? "Date(...)" : "Date()";
const formDesc20 = hasNew20 ? `constructs new Date()` : isOpt20 ? `calls Date?.()` : hasDateArgs20 ? `calls Date(...)` : `calls Date()`;
Comment on lines +1751 to +1755
line: loc21.line,
column: loc21.column,
start: tok.start,
end: memberTok21.end,
message:
Comment thread AGENTS.md Outdated
| SYN018 | (0.7+, warning) A fn body calls `Math.random()`, `Math?.random()`, or `Math.random?.()`. `Math.random` generates a random float at runtime but is invisible to botscript's capability model: `uses { random }` covers `random.*` stdlib namespace calls, not the `Math` global. A fn that calls `Math.random()` has an undeclared randomness dependency — callers cannot see it, tests cannot mock or suppress it the way they can the `random` stdlib. Detection: `Math` ident not preceded by `.`/`?.`, followed by `.` or `?.`, member is `random`, followed by `(` or `?.(`. Bare `Math.random` references without a trailing `(` are excluded. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Replace `Math.random()` with `random.next()` and add `uses { random }` to the fn header. If `Math.random` is required, wrap in `unsafe "uses Math.random for <reason>" { Math.random() }`. |
| SYN019 | (0.7+, warning) A fn body calls `crypto.getRandomValues(buf)` or `crypto.randomUUID()` (including optional-chain forms `crypto?.getRandomValues(buf)` and optional-call forms `crypto.getRandomValues?.(buf)`). These calls generate cryptographic randomness at runtime but are invisible to botscript's capability model: `uses { random }` covers `random.*` stdlib calls (`random.next()`, `random.int()`), not the `crypto` global. A fn that calls these methods has an undeclared randomness dependency — tests cannot control the output and callers cannot observe the dependency from the fn header. Detection: `crypto` ident not preceded by `.`/`?.`, followed by `.` or `?.`, followed by `getRandomValues` or `randomUUID`, followed by `(` or `?.(`. Member calls (`obj.crypto.getRandomValues(...)`), non-randomness members (e.g. `crypto.subtle.digest(...)`), bare references, and `fn`/`function` declarations named `crypto` are excluded. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Use `random.next()` or `random.int(min, max)` from the `random` stdlib with `uses { random }` when general randomness is sufficient. If cryptographic randomness or UUIDs are genuinely required, wrap in `unsafe "uses crypto for <reason>" { crypto.getRandomValues(buf) }`. |
| SYN020 | (0.7+, warning) A fn body calls `Date.now()`, constructs `new Date()` / `new Date` (no-arg ambient time), or calls `Date()` / `Date?.()`. These inject the current wallclock time at runtime but are invisible to botscript's capability model: `uses { time }` covers `time.*` stdlib calls, not the `Date` global. A fn that reads these has an undeclared time dependency — callers cannot see it and tests cannot control the clock value observed. Detection: `Date` ident not preceded by `.`/`?.`, followed by `.` or `?.` then `now`, or preceded by `new` / bare call form, followed by `(` or `?.(` or `)` (no-arg). `obj.Date.*`, member calls on a `Date` instance, and `fn`/`function` declarations named `Date` are excluded. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Pass `nowMs: number` as an explicit parameter so callers and tests can control the time value. If ambient time is required at an entry point, use `time.now()` from the `time` stdlib with `uses { time }`, or wrap in `unsafe "reads wallclock time for <reason>" { Date.now() }`. |
| SYN021 | (0.7+, warning) A fn body calls `performance.now()`, `performance?.now()`, `performance.now?.()`, or reads `performance.timeOrigin` / `performance?.timeOrigin`. These inject ambient timing information at runtime but are invisible to botscript's capability model: `uses { time }` covers `time.*` stdlib calls, not the `performance` global. Detection: `performance` ident not preceded by `.`/`?.`, followed by `.` or `?.`, member is `now` (with trailing `(` or `?.(`) or `timeOrigin`. `obj.performance.*` member calls, `fn`/`function` declarations named `performance`, and `unsafe {}` / `unsafe "reason" fn` bodies are suppressed. | Pass the start time as an explicit parameter. If monotonic ordering (not wallclock) is the goal, use `clock.sequence()` (no capability required). If `performance.now` is genuinely needed, wrap in `unsafe "uses performance.now for <reason>" { performance.now() }`. |
…t, end position

- SYN020: when isOpt20 + args present, render Date?.(…) not Date?.() so the
  diagnostic text accurately reflects the call form callers wrote
- SYN021: set end to afterNow21.start + 1 (the opening `(`) to match the
  convention in SYN004/SYN019 and point precisely at the call site

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Comment thread AGENTS.md Outdated
| SYN018 | (0.7+, warning) A fn body calls `Math.random()`, `Math?.random()`, or `Math.random?.()`. `Math.random` generates a random float at runtime but is invisible to botscript's capability model: `uses { random }` covers `random.*` stdlib namespace calls, not the `Math` global. A fn that calls `Math.random()` has an undeclared randomness dependency — callers cannot see it, tests cannot mock or suppress it the way they can the `random` stdlib. Detection: `Math` ident not preceded by `.`/`?.`, followed by `.` or `?.`, member is `random`, followed by `(` or `?.(`. Bare `Math.random` references without a trailing `(` are excluded. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Replace `Math.random()` with `random.next()` and add `uses { random }` to the fn header. If `Math.random` is required, wrap in `unsafe "uses Math.random for <reason>" { Math.random() }`. |
| SYN019 | (0.7+, warning) A fn body calls `crypto.getRandomValues(buf)` or `crypto.randomUUID()` (including optional-chain forms `crypto?.getRandomValues(buf)` and optional-call forms `crypto.getRandomValues?.(buf)`). These calls generate cryptographic randomness at runtime but are invisible to botscript's capability model: `uses { random }` covers `random.*` stdlib calls (`random.next()`, `random.int()`), not the `crypto` global. A fn that calls these methods has an undeclared randomness dependency — tests cannot control the output and callers cannot observe the dependency from the fn header. Detection: `crypto` ident not preceded by `.`/`?.`, followed by `.` or `?.`, followed by `getRandomValues` or `randomUUID`, followed by `(` or `?.(`. Member calls (`obj.crypto.getRandomValues(...)`), non-randomness members (e.g. `crypto.subtle.digest(...)`), bare references, and `fn`/`function` declarations named `crypto` are excluded. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Use `random.next()` or `random.int(min, max)` from the `random` stdlib with `uses { random }` when general randomness is sufficient. If cryptographic randomness or UUIDs are genuinely required, wrap in `unsafe "uses crypto for <reason>" { crypto.getRandomValues(buf) }`. |
| SYN020 | (0.7+, warning) A fn body calls `Date.now()`, constructs `new Date()` / `new Date` (no-arg ambient time), or calls `Date()` / `Date?.()`. These inject the current wallclock time at runtime but are invisible to botscript's capability model: `uses { time }` covers `time.*` stdlib calls, not the `Date` global. A fn that reads these has an undeclared time dependency — callers cannot see it and tests cannot control the clock value observed. Detection: `Date` ident not preceded by `.`/`?.`, followed by `.` or `?.` then `now`, or preceded by `new` / bare call form, followed by `(` or `?.(` or `)` (no-arg). `obj.Date.*`, member calls on a `Date` instance, and `fn`/`function` declarations named `Date` are excluded. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Pass `nowMs: number` as an explicit parameter so callers and tests can control the time value. If ambient time is required at an entry point, use `time.now()` from the `time` stdlib with `uses { time }`, or wrap in `unsafe "reads wallclock time for <reason>" { Date.now() }`. |
| SYN021 | (0.7+, warning) A fn body calls `performance.now()`, `performance?.now()`, `performance.now?.()`, or reads `performance.timeOrigin` / `performance?.timeOrigin`. These inject ambient timing information at runtime but are invisible to botscript's capability model: `uses { time }` covers `time.*` stdlib calls, not the `performance` global. Detection: `performance` ident not preceded by `.`/`?.`, followed by `.` or `?.`, member is `now` (with trailing `(` or `?.(`) or `timeOrigin`. `obj.performance.*` member calls, `fn`/`function` declarations named `performance`, and `unsafe {}` / `unsafe "reason" fn` bodies are suppressed. | Pass the start time as an explicit parameter. If monotonic ordering (not wallclock) is the goal, use `clock.sequence()` (no capability required). If `performance.now` is genuinely needed, wrap in `unsafe "uses performance.now for <reason>" { performance.now() }`. |
…() forward reference from AGENTS.md

Adds a regression test verifying SYN020 formats Date?.(x) as Date?.(...)
in the warning message when the optional call has arguments.

Removes the clock.sequence() reference from the SYN021 AGENTS.md fix-hint —
clock stdlib does not exist in main yet; the guidance was misleading.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Comment thread AGENTS.md Outdated
| SYN016 | (0.7+, warning) A fn body accesses `indexedDB.*` — any member access (`.`, `?.`) on the `indexedDB` global. `indexedDB` is same-origin persistent database storage invisible to botscript's capability model (`reads {}` / `writes {}` labels cover declared resource identifiers, not the Web Storage API). Unlike `localStorage`, `indexedDB` is asynchronous and has no practical size limit, making invisible access higher-impact. A fn that accesses `indexedDB` has undeclared persistent state dependencies invisible to callers and audit tooling. Detection: `indexedDB` ident not preceded by `.`/`?.`, followed by `.` or `?.`. Bare references and `fn`/`function` declarations named `indexedDB` are excluded. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Pass an `IDBDatabase` handle as an explicit fn parameter so callers control what database is accessed and tests can inject a mock. If direct global access is required, wrap in `unsafe "reads/writes indexedDB for <reason>" { indexedDB.open(name) }`. |
| SYN018 | (0.7+, warning) A fn body calls `Math.random()`, `Math?.random()`, or `Math.random?.()`. `Math.random` generates a random float at runtime but is invisible to botscript's capability model: `uses { random }` covers `random.*` stdlib namespace calls, not the `Math` global. A fn that calls `Math.random()` has an undeclared randomness dependency — callers cannot see it, tests cannot mock or suppress it the way they can the `random` stdlib. Detection: `Math` ident not preceded by `.`/`?.`, followed by `.` or `?.`, member is `random`, followed by `(` or `?.(`. Bare `Math.random` references without a trailing `(` are excluded. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Replace `Math.random()` with `random.next()` and add `uses { random }` to the fn header. If `Math.random` is required, wrap in `unsafe "uses Math.random for <reason>" { Math.random() }`. |
| SYN019 | (0.7+, warning) A fn body calls `crypto.getRandomValues(buf)` or `crypto.randomUUID()` (including optional-chain forms `crypto?.getRandomValues(buf)` and optional-call forms `crypto.getRandomValues?.(buf)`). These calls generate cryptographic randomness at runtime but are invisible to botscript's capability model: `uses { random }` covers `random.*` stdlib calls (`random.next()`, `random.int()`), not the `crypto` global. A fn that calls these methods has an undeclared randomness dependency — tests cannot control the output and callers cannot observe the dependency from the fn header. Detection: `crypto` ident not preceded by `.`/`?.`, followed by `.` or `?.`, followed by `getRandomValues` or `randomUUID`, followed by `(` or `?.(`. Member calls (`obj.crypto.getRandomValues(...)`), non-randomness members (e.g. `crypto.subtle.digest(...)`), bare references, and `fn`/`function` declarations named `crypto` are excluded. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Use `random.next()` or `random.int(min, max)` from the `random` stdlib with `uses { random }` when general randomness is sufficient. If cryptographic randomness or UUIDs are genuinely required, wrap in `unsafe "uses crypto for <reason>" { crypto.getRandomValues(buf) }`. |
| SYN020 | (0.7+, warning) A fn body calls `Date.now()`, constructs `new Date()` / `new Date` (no-arg ambient time), or calls `Date()` / `Date?.()`. These inject the current wallclock time at runtime but are invisible to botscript's capability model: `uses { time }` covers `time.*` stdlib calls, not the `Date` global. A fn that reads these has an undeclared time dependency — callers cannot see it and tests cannot control the clock value observed. Detection: `Date` ident not preceded by `.`/`?.`, followed by `.` or `?.` then `now`, or preceded by `new` / bare call form, followed by `(` or `?.(` or `)` (no-arg). `obj.Date.*`, member calls on a `Date` instance, and `fn`/`function` declarations named `Date` are excluded. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Pass `nowMs: number` as an explicit parameter so callers and tests can control the time value. If ambient time is required at an entry point, use `time.now()` from the `time` stdlib with `uses { time }`, or wrap in `unsafe "reads wallclock time for <reason>" { Date.now() }`. |
…s including no-parens new Date

The previous description's "followed by `(` or `?.(` or `)` (no-arg)"
phrasing implied all three SYN020 forms require a trailing delimiter, but
`new Date` without parentheses has neither — it's detected by the ABSENCE
of `(`, `<`, `?.`, or `.` after `Date`. Rewrite the detection description
to enumerate the three forms explicitly so contributors don't misread the
check when extending it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Comment thread AGENTS.md
| SYN016 | (0.7+, warning) A fn body accesses `indexedDB.*` — any member access (`.`, `?.`) on the `indexedDB` global. `indexedDB` is same-origin persistent database storage invisible to botscript's capability model (`reads {}` / `writes {}` labels cover declared resource identifiers, not the Web Storage API). Unlike `localStorage`, `indexedDB` is asynchronous and has no practical size limit, making invisible access higher-impact. A fn that accesses `indexedDB` has undeclared persistent state dependencies invisible to callers and audit tooling. Detection: `indexedDB` ident not preceded by `.`/`?.`, followed by `.` or `?.`. Bare references and `fn`/`function` declarations named `indexedDB` are excluded. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Pass an `IDBDatabase` handle as an explicit fn parameter so callers control what database is accessed and tests can inject a mock. If direct global access is required, wrap in `unsafe "reads/writes indexedDB for <reason>" { indexedDB.open(name) }`. |
| SYN018 | (0.7+, warning) A fn body calls `Math.random()`, `Math?.random()`, or `Math.random?.()`. `Math.random` generates a random float at runtime but is invisible to botscript's capability model: `uses { random }` covers `random.*` stdlib namespace calls, not the `Math` global. A fn that calls `Math.random()` has an undeclared randomness dependency — callers cannot see it, tests cannot mock or suppress it the way they can the `random` stdlib. Detection: `Math` ident not preceded by `.`/`?.`, followed by `.` or `?.`, member is `random`, followed by `(` or `?.(`. Bare `Math.random` references without a trailing `(` are excluded. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Replace `Math.random()` with `random.next()` and add `uses { random }` to the fn header. If `Math.random` is required, wrap in `unsafe "uses Math.random for <reason>" { Math.random() }`. |
| SYN019 | (0.7+, warning) A fn body calls `crypto.getRandomValues(buf)` or `crypto.randomUUID()` (including optional-chain forms `crypto?.getRandomValues(buf)` and optional-call forms `crypto.getRandomValues?.(buf)`). These calls generate cryptographic randomness at runtime but are invisible to botscript's capability model: `uses { random }` covers `random.*` stdlib calls (`random.next()`, `random.int()`), not the `crypto` global. A fn that calls these methods has an undeclared randomness dependency — tests cannot control the output and callers cannot observe the dependency from the fn header. Detection: `crypto` ident not preceded by `.`/`?.`, followed by `.` or `?.`, followed by `getRandomValues` or `randomUUID`, followed by `(` or `?.(`. Member calls (`obj.crypto.getRandomValues(...)`), non-randomness members (e.g. `crypto.subtle.digest(...)`), bare references, and `fn`/`function` declarations named `crypto` are excluded. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Use `random.next()` or `random.int(min, max)` from the `random` stdlib with `uses { random }` when general randomness is sufficient. If cryptographic randomness or UUIDs are genuinely required, wrap in `unsafe "uses crypto for <reason>" { crypto.getRandomValues(buf) }`. |
| SYN020 | (0.7+, warning) A fn body calls `Date.now()`, constructs `new Date()` / `new Date` (no-arg ambient time), or calls `Date()` / `Date?.()`. These inject the current wallclock time at runtime but are invisible to botscript's capability model: `uses { time }` covers `time.*` stdlib calls, not the `Date` global. A fn that reads these has an undeclared time dependency — callers cannot see it and tests cannot control the clock value observed. Detection has three forms: (1) member call — `Date` ident (not preceded by `.`/`?.`) followed by `.` or `?.` then `now`, then `(` or `?.(` or `?.(`; (2) no-parens construction — `Date` preceded by `new` and NOT followed by `(`, `<`, `?.`, or `.` (JS/TS `new Date` without parens is equivalent to `new Date()`); (3) bare call — `Date` followed by `(` or `?.(` without a preceding `new`. Excluded: `new Date(arg)` with explicit arguments (constructs a specific date, not ambient time), `obj.Date.*`, member calls on a `Date` instance, and `fn`/`function` declarations named `Date`. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Pass `nowMs: number` as an explicit parameter so callers and tests can control the time value. If ambient time is required at an entry point, use `time.now()` from the `time` stdlib with `uses { time }`, or wrap in `unsafe "reads wallclock time for <reason>" { Date.now() }`. |
Comment thread AGENTS.md
| SYN018 | (0.7+, warning) A fn body calls `Math.random()`, `Math?.random()`, or `Math.random?.()`. `Math.random` generates a random float at runtime but is invisible to botscript's capability model: `uses { random }` covers `random.*` stdlib namespace calls, not the `Math` global. A fn that calls `Math.random()` has an undeclared randomness dependency — callers cannot see it, tests cannot mock or suppress it the way they can the `random` stdlib. Detection: `Math` ident not preceded by `.`/`?.`, followed by `.` or `?.`, member is `random`, followed by `(` or `?.(`. Bare `Math.random` references without a trailing `(` are excluded. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Replace `Math.random()` with `random.next()` and add `uses { random }` to the fn header. If `Math.random` is required, wrap in `unsafe "uses Math.random for <reason>" { Math.random() }`. |
| SYN019 | (0.7+, warning) A fn body calls `crypto.getRandomValues(buf)` or `crypto.randomUUID()` (including optional-chain forms `crypto?.getRandomValues(buf)` and optional-call forms `crypto.getRandomValues?.(buf)`). These calls generate cryptographic randomness at runtime but are invisible to botscript's capability model: `uses { random }` covers `random.*` stdlib calls (`random.next()`, `random.int()`), not the `crypto` global. A fn that calls these methods has an undeclared randomness dependency — tests cannot control the output and callers cannot observe the dependency from the fn header. Detection: `crypto` ident not preceded by `.`/`?.`, followed by `.` or `?.`, followed by `getRandomValues` or `randomUUID`, followed by `(` or `?.(`. Member calls (`obj.crypto.getRandomValues(...)`), non-randomness members (e.g. `crypto.subtle.digest(...)`), bare references, and `fn`/`function` declarations named `crypto` are excluded. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Use `random.next()` or `random.int(min, max)` from the `random` stdlib with `uses { random }` when general randomness is sufficient. If cryptographic randomness or UUIDs are genuinely required, wrap in `unsafe "uses crypto for <reason>" { crypto.getRandomValues(buf) }`. |
| SYN020 | (0.7+, warning) A fn body calls `Date.now()`, constructs `new Date()` / `new Date` (no-arg ambient time), or calls `Date()` / `Date?.()`. These inject the current wallclock time at runtime but are invisible to botscript's capability model: `uses { time }` covers `time.*` stdlib calls, not the `Date` global. A fn that reads these has an undeclared time dependency — callers cannot see it and tests cannot control the clock value observed. Detection has three forms: (1) member call — `Date` ident (not preceded by `.`/`?.`) followed by `.` or `?.` then `now`, then `(` or `?.(` or `?.(`; (2) no-parens construction — `Date` preceded by `new` and NOT followed by `(`, `<`, `?.`, or `.` (JS/TS `new Date` without parens is equivalent to `new Date()`); (3) bare call — `Date` followed by `(` or `?.(` without a preceding `new`. Excluded: `new Date(arg)` with explicit arguments (constructs a specific date, not ambient time), `obj.Date.*`, member calls on a `Date` instance, and `fn`/`function` declarations named `Date`. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Pass `nowMs: number` as an explicit parameter so callers and tests can control the time value. If ambient time is required at an entry point, use `time.now()` from the `time` stdlib with `uses { time }`, or wrap in `unsafe "reads wallclock time for <reason>" { Date.now() }`. |
| SYN021 | (0.7+, warning) A fn body calls `performance.now()`, `performance?.now()`, `performance.now?.()`, or reads `performance.timeOrigin` / `performance?.timeOrigin`. These inject ambient timing information at runtime but are invisible to botscript's capability model: `uses { time }` covers `time.*` stdlib calls, not the `performance` global. Detection: `performance` ident not preceded by `.`/`?.`, followed by `.` or `?.`, member is `now` (with trailing `(` or `?.(`) or `timeOrigin`. `obj.performance.*` member calls, `fn`/`function` declarations named `performance`, and `unsafe {}` / `unsafe "reason" fn` bodies are suppressed. | Pass the start time as an explicit parameter so callers control the clock value. If `performance.now` is genuinely needed (e.g. for high-resolution elapsed time), wrap in `unsafe "uses performance.now for <reason>" { performance.now() }`. |
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants