Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
38a6cce
feat(compiler): SYN017 — warn on Notification() construction that byp…
Jun 14, 2026
f8f7405
fix(compiler): address Copilot review on SYN017 — add optional-call a…
Jun 14, 2026
02261b7
fix(syn017): address Copilot review — generator declaration exclusion…
Jun 14, 2026
c49f842
fix(syn017): use operator token kind for generator * exclusion
Jun 14, 2026
55bd487
fix(syn017): exclude type-literal method signatures with no type anno…
Jun 15, 2026
13449e5
fix(syn017): narrow type-literal exclusion to guard against object-li…
Jun 16, 2026
863c774
docs(mcp): fix grammar nit in SYN017 explanation text
Jun 16, 2026
55797d7
fix(syn017): extend type-literal exclusion to handle semicolon/comma …
Jun 16, 2026
4e1b8c1
fix(syn017): bound Notification<T> generic scan to decl.tokenEnd
Jun 16, 2026
053fa4a
fix(syn017): address APPROVED review nits — title wording and header …
Jun 17, 2026
c92cfd9
refactor(compiler): use isFunctionStarDecl helper in SYN017 generator…
Jun 17, 2026
760d1ee
fix(SYN017): use (...) placeholder in unsafe-wrap message, not (title…
Jun 17, 2026
9996ff5
fix(compiler): SYN017 add empty-parens type-literal test; fix MCP exa…
Jun 17, 2026
0624e63
fix(compiler): align SYN017 warning message unsafe reason with regist…
Jun 17, 2026
16f7157
fix(compiler): SYN017 empty-parens type-literal exclusion now covers …
Jun 17, 2026
18ec868
fix(syn017): broaden TS type-literal exclusion; align title strings
Jun 18, 2026
01ade43
fix(SYN017): address Copilot review — mark await-in-ternary test fixt…
Jun 18, 2026
ef96f10
fix(syn017): extend type-literal exclusion to non-first generic type …
Jun 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ parse the resulting `{ ok: false, diagnostics: [...] }` envelope.
| SYN013 | (0.7+, warning) A fn body constructs a `Worker` or `SharedWorker` via `new Worker(scriptURL)`, bare `Worker(scriptURL)`, `Worker?.(scriptURL)`, `new SharedWorker(scriptURL)`, `SharedWorker?.(scriptURL)`, or TypeScript instantiation forms. Worker construction spawns a new JS execution context whose capability surface is unbounded — the worker script can make network requests, access storage, and perform any operation, none of which is visible in the spawning fn's `uses {}`, `reads {}`, or `writes {}` declarations. CAP001 cannot infer any capability from worker construction. Detection: `Worker` or `SharedWorker` not preceded by `.`/`?.`, followed by `(`, `?.(`, or `<T>(` (generic scan gated on `new`). Member calls, `function`/`fn`/`function*` declarations, object method shorthands, and TS method signatures are excluded. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Wrap in `unsafe "<reason>" { new Worker(scriptURL) }` with a reason that documents what capabilities the worker script is expected to use. |
| SYN014 | (0.7+, warning) A fn body calls `new BroadcastChannel(name)`, `BroadcastChannel(name)`, or TypeScript instantiation form `new BroadcastChannel<T>(name)`. `BroadcastChannel` opens a cross-context message channel at runtime — any tab, window, or worker on the same origin can post to or receive from it — invisible to CAP001. A fn that constructs a BroadcastChannel has an undeclared cross-context messaging dependency. Detection: `BroadcastChannel` not preceded by `.`/`?.`, followed by `(` or `?.(` — or `<T>(` when preceded by `new` (generic scan is gated on `new` to avoid `<`/`>` comparison false-positives). Member calls (`obj.BroadcastChannel(...)`), object method shorthands, TypeScript method signatures, and `fn`/`function` declarations named `BroadcastChannel` are excluded. The `:` exclusion is guarded against ternary consequents. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Wrap in `unsafe "<reason>" { new BroadcastChannel(name) }` (or matching call form) to make the cross-context messaging dependency visible in the diff. |
| 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) }`. |
| SYN017 | (0.7+, warning) A fn body constructs or calls `Notification` via `new Notification(title)`, bare `Notification(title)`, `Notification?.(title)`, or TypeScript instantiation `new Notification<T>(title)`. `Notification` dispatches a user-visible browser notification at runtime — a UI side effect invisible to botscript's capability model: no `uses {}`, `reads {}`, or `writes {}` declaration covers notification dispatch. Callers cannot observe, audit, or suppress the effect from the fn's declared surface. Detection: `Notification` ident not preceded by `.`/`?.`, followed by `(` or `?.(` (or `<T>(` when `new` precedes). Member calls, fn/function declarations named `Notification`, object method shorthands, and TypeScript method signatures (including optional-param forms) are excluded. `unsafe {}` blocks and `unsafe "reason" fn` bodies are suppressed. | Pass a notification-dispatch callback as an explicit fn parameter so callers control whether a notification fires. If direct access is required, wrap in `unsafe "sends notification for <reason>" { new Notification(title, options) }`. |
| 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) }`. |
| SYN022 | (0.7+, warning) A fn body accesses `process.argv`, `process.cwd()`, `process.platform`, `process.arch`, `process.pid`, `process.ppid`, `process.version`, `process.versions`, `process.hrtime()`, `process.uptime()`, `process.memoryUsage()`, `process.cpuUsage()`, or `process.resourceUsage()`. These read ambient Node.js process state at runtime — OS identity, process tree, memory, CPU — invisible to botscript's capability model. Unlike `process.env` (SYN005) and `process.exit` (SYN006), which cover configuration and termination respectively, SYN022 targets the remaining ambient introspection surface. Detection: `process` ident not preceded by `.`/`?.`, followed by `.` or `?.`, member in the ambient-state set, optionally followed by `(` or `?.(`. Bare `process.*` references without a trailing `(` still fire. `obj.process.*` member calls, `unsafe {}` blocks, and `unsafe "reason" fn` bodies are excluded. `process.env` and `process.exit` are handled by SYN005/SYN006 and excluded here. | Pass the required ambient value as an explicit fn parameter so the dependency is visible in the call signature and tests can inject a mock. If direct `process.*` access is required at a bootstrap entry point, wrap in `unsafe "reads process state for <reason>" { process.argv }`. |
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ claude mcp add botscript -- npx -y @mbfarias/botscript-mcp
| ----------- | -------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `primer` | (no args) | The canonical language primer (same text the `?primer` directive emits). |
| `transform` | `{ source: string, filename?: string }` | `{ ok: true, code, forms, version, warnings: [...] }` on success, or `{ ok: false, diagnostics: [...] }` on failure. `warnings` is an array of non-blocking diagnostics (e.g. CAP003). |
| `explain` | `{ code: string }` | Long-form explanation for any stable diagnostic code (`ALI001`, `ALI002`, `ALI003`, `BS001`, `BS002`, `CAP001`–`CAP003`, `DEP001`–`DEP004`, `EFF002`–`EFF004`, `FMT001`, `INT001`–`INT005`, `MAT001`–`MAT004`, `RES001`, `RES002`, `SYN001`, `SYN002`, `SYN003`, `SYN004`, `SYN005`, `SYN006`, `SYN007`, `SYN008`, `SYN010`, `SYN011`, `SYN012`, `SYN013`, `SYN014`, `SYN016`, `SYN018`, `SYN019`, `SYN022`, `SYN023`, `THR001`–`THR004`, `UNS001`–`UNS005`, `VER001`–`VER003`) plus a fails/passes example pair. |
| `explain` | `{ code: string }` | Long-form explanation for any stable diagnostic code (`ALI001`, `ALI002`, `ALI003`, `BS001`, `BS002`, `CAP001`–`CAP003`, `DEP001`–`DEP004`, `EFF002`–`EFF004`, `FMT001`, `INT001`–`INT005`, `MAT001`–`MAT004`, `RES001`, `RES002`, `SYN001`, `SYN002`, `SYN003`, `SYN004`, `SYN005`, `SYN006`, `SYN007`, `SYN008`, `SYN010`, `SYN011`, `SYN012`, `SYN013`, `SYN014`, `SYN016`, `SYN017`, `SYN018`, `SYN019`, `SYN022`, `SYN023`, `THR001`–`THR004`, `UNS001`–`UNS005`, `VER001`–`VER003`) plus a fails/passes example pair. |

A bot's loop becomes deterministic: `transform` → if `ok=false`, read
`diagnostics[0].code` → `explain(code)` → apply `rewrite` → `transform` again.
Expand Down
33 changes: 33 additions & 0 deletions packages/compiler/src/error-codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -914,6 +914,39 @@ const E: Record<string, ErrorCodeEntry> = {
" return new Promise((resolve) => { req.onsuccess = (e) => resolve(e.target.result) })\n" +
"}",
},
SYN017: {
code: "SYN017",
title: "new Notification() / Notification() call bypasses the capability model",
rule:
"`new Notification(title)`, bare `Notification(title)`, optional-call `Notification?.(title)`, " +
"and TypeScript generic form `new Notification<T>(title)` calls create user-visible browser " +
"notifications at runtime — a side effect entirely invisible to botscript's capability model. " +
"No `uses {}`, `reads {}`, or `writes {}` declaration covers notification dispatch: callers " +
"cannot observe, audit, or suppress the UI effect from the fn's declared surface.",
idiom:
"accept a notification-dispatch callback as an explicit fn parameter so callers control " +
"whether a notification is shown and tests can capture or suppress it; " +
"if direct `Notification` access is required, wrap in " +
"`unsafe \"sends browser notification for <reason>\" { new Notification(title, options) }`",
rewrite:
"// before — notification dispatch invisible to the capability model\n" +
"fn alertUser(msg: string) -> void {\n" +
" new Notification(msg) // SYN017\n" +
"}\n\n" +
"// after — dispatch function passed as parameter; callers control UI side effect\n" +
"fn alertUser(notify: (msg: string) => void, msg: string) -> void {\n" +
" notify(msg)\n" +
"}",
example:
"// SYN017: Notification dispatch bypasses the capability model\n" +
"fn warnUser(title: string, body: string) -> void {\n" +
" new Notification(title, { body }) // SYN017\n" +
"}\n\n" +
"// fix: wrap in unsafe with a reason\n" +
"fn warnUser(title: string, body: string) -> void {\n" +
" unsafe \"shows alert notification for user-triggered warning\" { new Notification(title, { body }) }\n" +
"}",
},
SYN018: {
code: "SYN018",
title: "Math.random() call bypasses the random capability model",
Expand Down
Loading
Loading