Skip to content

fix(engine): scan top-level export declarations with a null source - #493

Merged
rhuanbarreto merged 2 commits into
archgate:mainfrom
hancrafted:fix/scan-top-level-export-source-null
Jul 24, 2026
Merged

fix(engine): scan top-level export declarations with a null source#493
rhuanbarreto merged 2 commits into
archgate:mainfrom
hancrafted:fix/scan-top-level-export-source-null

Conversation

@hancrafted

Copy link
Copy Markdown
Contributor

Splits the scanner fix out of #491 (now closed) into a standalone PR, as suggested there.

The bug

ESTree sets source: null on any export declaration with no from clause — export function, export const, export { local }. AstNodeSchema typed source as optional-only, so safeParse failed on the node, parseNode returned null, and the walk (if (child) walk(child)) skipped the node and its entire subtree.

A banned global, eval, or a dynamic import("node:child_process") nested inside a top-level export was therefore never scanned — a silent false-negative that archgate check reports as a pass.

The fix

Make source .nullable().optional() in the schema (and AstNode.source?: AstNode | null) so the node stays in the walk. checkModuleSpecifier already no-ops on a null source, so re-exports (export { x } from "…") are unaffected. No behavior change beyond no longer dropping these nodes.

Tests

Three regression tests in rule-scanner.test.ts:

  • banned global inside export function — unscanned before, caught now
  • banned global inside export const — unscanned before, caught now
  • export { x } from "node:fs" — positive control, confirms re-export scanning still fires

bun run validate passes locally: oxlint, tsc --build, oxfmt --check, 1579 tests, archgate check 44/44, compile.

The commit is DCO signed-off.

ESTree sets `source: null` on an `export` declaration that has no `from`
clause (`export function`, `export const`, `export { local }`). The AST node
schema typed `source` as optional-only, so Zod validation failed on the node,
`parseNode` dropped it, and the walk skipped its entire subtree.

Anything dangerous inside a top-level export — a banned global, `eval`, a
dynamic `import("node:child_process")` — therefore went unscanned, a silent
false-negative that `archgate check` reported as a pass. The schema now
tolerates the null so the node stays in the walk; `checkModuleSpecifier`
already no-ops on a null source, so re-exports are unaffected.

Adds regression tests: a banned global inside `export function` and
`export const` (previously unscanned), plus `export { x } from "node:fs"`
as a positive control that re-export scanning still fires.

Signed-off-by: hancrafted <hancrafted@users.noreply.github.com>
@hancrafted
hancrafted requested a review from rhuanbarreto as a code owner July 23, 2026 09:14
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 95e12098-2a8e-43a8-a367-26f7697d5b6e

📥 Commits

Reviewing files that changed from the base of the PR and between 6eff614 and 4fb9aa5.

📒 Files selected for processing (1)
  • tests/engine/rule-scanner.test.ts
📜 Recent review details
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx,js,jsx,mjs,cjs}

📄 CodeRabbit inference engine (.archgate/adrs/ARCH-006-dependency-policy.md)

**/*.{ts,tsx,js,jsx,mjs,cjs}: Use Bun built-ins for file I/O (Bun.file, Bun.write), HTTP, subprocess execution (Bun.spawn), globbing (Bun.Glob), and testing (bun:test).
Do not use Node.js-specific APIs when Bun alternatives exist; for example, use Bun.file() instead of fs.readFile() for simple reads.
Prefer node: built-in modules such as node:util, node:path, and node:fs over npm alternatives.
Do not use utility libraries for single functions, such as importing lodash for one helper like pick.
Do not use path aliases (tsconfig paths); use relative imports with Bun's native module resolution.
Do not install packages globally during development; use bunx for one-off tools.

Files:

  • tests/engine/rule-scanner.test.ts
tests/**/*.ts

📄 CodeRabbit inference engine (.archgate/adrs/ARCH-009-platform-detection-helper.md)

In test files, use _resetPlatformCache() to simulate different platforms instead of mocking or mutating process.platform directly.

tests/**/*.ts: Use Bun's built-in bun:test runner for all tests; do not import test utilities from node:test.
Shared test helpers must also use restoreEnv when restoring environment variables.

Files:

  • tests/engine/rule-scanner.test.ts
{src,tests}/**/*.ts

📄 CodeRabbit inference engine (.archgate/adrs/LEGAL-001-spdx-license-headers.md)

{src,tests}/**/*.ts: Every TypeScript source file in src/ and tests/ must begin with // SPDX-License-Identifier: Apache-2.0 followed by // Copyright 2026 Archgate.
If a TypeScript file has a shebang line (for example #!/usr/bin/env bun in src/cli.ts), the SPDX license header must appear immediately after the shebang.
Use single-line // comments for the SPDX header; do not use block comments (/* */) or alternate license identifiers.

Files:

  • tests/engine/rule-scanner.test.ts
tests/**/*.test.ts

📄 CodeRabbit inference engine (.archgate/adrs/ARCH-005-testing-standards.md)

tests/**/*.test.ts: Place tests in tests/ mirroring the src/ directory structure, and name test files with the .test.ts suffix.
Test public module interfaces rather than private implementation details, use descriptive test names, and do not depend on network access.
Use isolated temporary directories created with mkdtemp for filesystem tests, and clean them up in afterEach or afterAll. Do not use hardcoded user or system paths.
Close external SDK instances such as servers, clients, and transports in afterEach or afterAll; manage their lifecycle in test hooks rather than individual test bodies.
When a temporary git repository performs commits, configure local user.email and user.name immediately after git init; do not rely on global git identity.
Every runnable test() or it() must contain at least one expect() assertion. Make implicit no-throw contracts explicit with not.toThrow() or resolves.toBeUndefined(). Use test.skip or test.todo for intentional placeholders.
Use test.skipIf(condition), test.skip, or test.todo for conditional or intentionally disabled tests; do not use bare early returns or empty callbacks to skip tests, and do not skip without a tracking issue.
When adding assertions to an older test file, import expect from bun:test.
Mock HTTP requests by assigning directly to globalThis.fetch, and restore the original fetch implementation or mock in afterEach; do not mock node:fetch.
Mock first-party modules with import * as mod and spyOn(mod, "fn"), restoring spies after each test; do not use process-global mock.module() for first-party modules.
Wrap inline spyOn or mockImplementation usage in try/finally so mockRestore() always runs, or manage spies in beforeEach and afterEach.
When redirecting user-scope paths, mock node:os's homedir() rather than relying on runtime HOME overrides; use environment overrides only for code that reads environment variables directly at call time.
Re...

Files:

  • tests/engine/rule-scanner.test.ts
tests/engine/rule-scanner.test.ts

📄 CodeRabbit inference engine (.archgate/adrs/ARCH-024-rule-file-sandbox-boundary.md)

Preserve scanner diagnostic and source-position coverage for blocked identifiers and properties.

Files:

  • tests/engine/rule-scanner.test.ts
**

⚙️ CodeRabbit configuration file

**: This project uses Archgate — an AI governance framework based on
Architecture Decision Records (ADRs). The ADRs in .archgate/adrs/
are the authoritative rules for this codebase. Each ADR has a companion
.rules.ts file with automated checks that run via archgate check.

When reviewing, you must:

  1. Treat ADR violations as blocking issues, not suggestions.
  2. Cite the specific ADR ID when flagging a violation (e.g., "Violates ARCH-006").
  3. Focus on semantic and contextual violations that automated rules cannot catch —
    the .rules.ts files already cover syntactic/structural patterns.
  4. If you are unsure whether something violates an ADR, flag it as a question
    rather than approving it.

Files:

  • tests/engine/rule-scanner.test.ts
🔇 Additional comments (1)
tests/engine/rule-scanner.test.ts (1)

116-125: LGTM!

Also applies to: 127-133


📝 Walkthrough

Walkthrough

Updated AST validation to accept source: null for top-level export declarations without a from clause. This prevents those nodes from being discarded during parsing, allowing child content to be scanned while module checks remain inactive when no source exists. Added regression tests covering banned globals inside exported functions and constants, plus banned modules in re-export declarations.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: scanning top-level exports when ESTree source is null.
Description check ✅ Passed The description directly matches the scanner fix and the added regression tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/engine/rule-scanner.test.ts`:
- Around line 97-122: Add a test in the “top-level export declarations are
scanned” suite covering a local export without a from clause, such as exporting
fetch under an alias, and assert that the scan reports the banned-global
violation. Keep the existing declaration-export and re-export cases unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d4e5ab97-8eaa-41b5-ac43-bd4f4622dbd4

📥 Commits

Reviewing files that changed from the base of the PR and between cd63671 and 6eff614.

📒 Files selected for processing (2)
  • src/engine/rule-scanner.ts
  • tests/engine/rule-scanner.test.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (12)
**/*.{ts,tsx,js,jsx,mjs,cjs}

📄 CodeRabbit inference engine (.archgate/adrs/ARCH-006-dependency-policy.md)

**/*.{ts,tsx,js,jsx,mjs,cjs}: Use Bun built-ins for file I/O (Bun.file, Bun.write), HTTP, subprocess execution (Bun.spawn), globbing (Bun.Glob), and testing (bun:test).
Do not use Node.js-specific APIs when Bun alternatives exist; for example, use Bun.file() instead of fs.readFile() for simple reads.
Prefer node: built-in modules such as node:util, node:path, and node:fs over npm alternatives.
Do not use utility libraries for single functions, such as importing lodash for one helper like pick.
Do not use path aliases (tsconfig paths); use relative imports with Bun's native module resolution.
Do not install packages globally during development; use bunx for one-off tools.

Files:

  • tests/engine/rule-scanner.test.ts
  • src/engine/rule-scanner.ts
tests/**/*.ts

📄 CodeRabbit inference engine (.archgate/adrs/ARCH-009-platform-detection-helper.md)

In test files, use _resetPlatformCache() to simulate different platforms instead of mocking or mutating process.platform directly.

tests/**/*.ts: Use Bun's built-in bun:test runner for all tests; do not import test utilities from node:test.
Shared test helpers must also use restoreEnv when restoring environment variables.

Files:

  • tests/engine/rule-scanner.test.ts
{src,tests}/**/*.ts

📄 CodeRabbit inference engine (.archgate/adrs/LEGAL-001-spdx-license-headers.md)

{src,tests}/**/*.ts: Every TypeScript source file in src/ and tests/ must begin with // SPDX-License-Identifier: Apache-2.0 followed by // Copyright 2026 Archgate.
If a TypeScript file has a shebang line (for example #!/usr/bin/env bun in src/cli.ts), the SPDX license header must appear immediately after the shebang.
Use single-line // comments for the SPDX header; do not use block comments (/* */) or alternate license identifiers.

Files:

  • tests/engine/rule-scanner.test.ts
  • src/engine/rule-scanner.ts
tests/**/*.test.ts

📄 CodeRabbit inference engine (.archgate/adrs/ARCH-005-testing-standards.md)

tests/**/*.test.ts: Place tests in tests/ mirroring the src/ directory structure, and name test files with the .test.ts suffix.
Test public module interfaces rather than private implementation details, use descriptive test names, and do not depend on network access.
Use isolated temporary directories created with mkdtemp for filesystem tests, and clean them up in afterEach or afterAll. Do not use hardcoded user or system paths.
Close external SDK instances such as servers, clients, and transports in afterEach or afterAll; manage their lifecycle in test hooks rather than individual test bodies.
When a temporary git repository performs commits, configure local user.email and user.name immediately after git init; do not rely on global git identity.
Every runnable test() or it() must contain at least one expect() assertion. Make implicit no-throw contracts explicit with not.toThrow() or resolves.toBeUndefined(). Use test.skip or test.todo for intentional placeholders.
Use test.skipIf(condition), test.skip, or test.todo for conditional or intentionally disabled tests; do not use bare early returns or empty callbacks to skip tests, and do not skip without a tracking issue.
When adding assertions to an older test file, import expect from bun:test.
Mock HTTP requests by assigning directly to globalThis.fetch, and restore the original fetch implementation or mock in afterEach; do not mock node:fetch.
Mock first-party modules with import * as mod and spyOn(mod, "fn"), restoring spies after each test; do not use process-global mock.module() for first-party modules.
Wrap inline spyOn or mockImplementation usage in try/finally so mockRestore() always runs, or manage spies in beforeEach and afterEach.
When redirecting user-scope paths, mock node:os's homedir() rather than relying on runtime HOME overrides; use environment overrides only for code that reads environment variables directly at call time.
Re...

Files:

  • tests/engine/rule-scanner.test.ts
tests/engine/rule-scanner.test.ts

📄 CodeRabbit inference engine (.archgate/adrs/ARCH-024-rule-file-sandbox-boundary.md)

Preserve scanner diagnostic and source-position coverage for blocked identifiers and properties.

Files:

  • tests/engine/rule-scanner.test.ts
**

⚙️ CodeRabbit configuration file

**: This project uses Archgate — an AI governance framework based on
Architecture Decision Records (ADRs). The ADRs in .archgate/adrs/
are the authoritative rules for this codebase. Each ADR has a companion
.rules.ts file with automated checks that run via archgate check.

When reviewing, you must:

  1. Treat ADR violations as blocking issues, not suggestions.
  2. Cite the specific ADR ID when flagging a violation (e.g., "Violates ARCH-006").
  3. Focus on semantic and contextual violations that automated rules cannot catch —
    the .rules.ts files already cover syntactic/structural patterns.
  4. If you are unsure whether something violates an ADR, flag it as a question
    rather than approving it.

Files:

  • tests/engine/rule-scanner.test.ts
  • src/engine/rule-scanner.ts
src/**/*.ts

📄 CodeRabbit inference engine (.archgate/adrs/ARCH-004-no-barrel-files.md)

src/**/*.ts: Do not re-export symbols from another module in any source file; statements like export { X } from "./other" and export type { X } from "./other" are forbidden.
Import symbols directly from the module that defines them; do not import from a directory path such as ../formats expecting implicit index.ts resolution.

src/**/*.ts: Use await Bun.file(path).json() when reading JSON files in Bun TypeScript source code; do not use JSON.parse(await Bun.file(path).text()) or JSON.parse(fs.readFileSync(path, "utf-8")) for file reads.
Use Bun.JSONC.parse() when reading files that may contain comments, such as tsconfig.json, instead of plain JSON.parse() on file contents.
Reserve JSON.parse() for parsing JSON strings from non-file sources such as API responses or string variables; do not use it as the default for reading JSON files.

src/**/*.ts: In TypeScript source files under src/, use Bun.env instead of process.env for all environment variable reads and writes; process.env must not be used.
In TypeScript source files under src/, use nullish coalescing for environment-variable defaults, e.g. Bun.env.NODE_ENV ?? "production".
In TypeScript source files under src/, use Boolean(Bun.env.CI) for truthy checks on environment flags.
In TypeScript source files under src/, do not destructure Bun.env (for example, const { HOME } = Bun.env); access properties individually instead.
In TypeScript source files under src/, do not reference process.env even in comments that suggest using it.

src/**/*.ts: Heavy dependencies such as inquirer, posthog-node, @sentry/*, and any similarly expensive runtime-only dependency MUST be loaded with dynamic import() at the point of use, never as a static value import at module top level.
Type-only references to heavy modules must use import type (for example, import type { PostHog } from "posthog-node" or import type * as SentryNs from "@sentry/node-core/light"); type-only...

Files:

  • src/engine/rule-scanner.ts
src/**/!(*platform).ts

📄 CodeRabbit inference engine (.archgate/adrs/ARCH-009-platform-detection-helper.md)

src/**/!(*platform).ts: In src/ TypeScript source files, do not read process.platform directly; use src/helpers/platform.ts (isWindows(), isMacOS(), isLinux(), isWSL(), getPlatformInfo()) for all platform detection.
Use the centralized platform helper instead of duplicating OS/WSL detection logic inline anywhere in src/ TypeScript source.
When behavior differs between Linux and Windows, account for WSL by using isWSL() rather than assuming `

Files:

  • src/engine/rule-scanner.ts
src/{helpers,engine}/**/*.ts

📄 CodeRabbit inference engine (.archgate/adrs/ARCH-002-error-handling.md)

Do not use console.log(), console.warn(), or console.info() directly in helper or engine files; use logInfo() or logWarn() instead.

Files:

  • src/engine/rule-scanner.ts
src/engine/**/*.ts

📄 CodeRabbit inference engine (.archgate/adrs/ARCH-023-engine-file-listing-via-in-memory-git-tracked-matching.md)

src/engine/**/*.ts: In the rules engine, list project files by matching glob patterns in memory against the git-tracked file set rather than walking the filesystem.
Route new engine file listings through listMatchingFiles for rule-facing inputs or matchTrackedFiles for trusted ADR frontmatter patterns.
When the target is a Git repository and respectGitignore is not false, pass the tracked set from getGitTrackedFiles to matching operations.
Per-run RunCaches must share glob results keyed by pattern and tracked mode and file text keyed by absolute path; cached promises should share in-flight work, and returned glob arrays must be copied before exposing them to rules.
Do not cache readJSON results because rules receive mutable objects whose shared mutation could leak between rules.
Do not filter filesystem scan results against the tracked set as a substitute for in-memory matching, and do not hardcode ignore directories; use the Git-derived tracked set instead.

Files:

  • src/engine/rule-scanner.ts
src/engine/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.archgate/adrs/ARCH-022-ast-aware-rule-context.md)

src/engine/**/*.{ts,tsx}: For TypeScript and JavaScript, ctx.ast() must reuse the in-process meriyah parser and must not spawn a subprocess.
Python and Ruby AST parsing must use only their interpreters' standard-library facilities through guarded Bun.spawn invocations; no third-party parser, native binding, or WASM grammar may be introduced.
Before any Python or Ruby subprocess is spawned, execute guardrails in exactly this order: path safety via safePath(), language plausibility validation, interpreter availability probing, then guarded invocation.
Use array-based Bun.spawn arguments only for Python/Ruby AST execution; never shell-interpolate paths or file contents.
Run Python AST subprocesses with python -I -c ... isolation, and strip a leading UTF-8 BOM before parsing Python and Ruby source.
Cache Python/Ruby interpreter availability once per check invocation rather than probing once per file.
ctx.ast() must throw on missing interpreters and parse failures, with distinguishable error messages; it must never return null or another silent-failure sentinel.
Support ast(path, language, { rev: "base" }) and fileAtBase(path) using the merge base of --base and HEAD. Base AST access must throw for an unresolved base or missing base file, while fileAtBase() returns null for those cases.
When { comments: true } is requested, attach structured comments with type, delimiter-stripped value, and source loc; omit comments unless explicitly requested. Ruby comment extraction must currently throw as unsupported.
Extract TypeScript/JavaScript comments from the original source, not transpiled output, and do not treat regex literals as comment-aware; Python comments must use tokenize.
Do not trust node.loc for TypeScript AST results because parsing transpiled output changes positions; re-locate constructs in the original source before reporting lines. JavaScript locations are source-accurate.
Do not invoke Bun.spawn, child_process, git, ...

Files:

  • src/engine/rule-scanner.ts
src/engine/rule-scanner.ts

📄 CodeRabbit inference engine (.archgate/adrs/ARCH-022-ast-aware-rule-context.md)

Factor duplicated parseModule() calls into a shared exported parse helper used by both rule-scanner.ts and the TypeScript/JavaScript ctx.ast() implementation.

src/engine/rule-scanner.ts: Statically scan every .rules.ts source with scanRuleSource() and require zero violations before execution.
Use an allowlist, never a denylist, for module specifiers; only explicitly permitted modules may be imported.
Allow only the node:path, node:url, node:util, and node:crypto specifiers; never allow their bare equivalents or node:module.
Route static imports, literal and non-literal dynamic imports, named re-exports, and star re-exports through the same module-specifier check; reject non-literal dynamic imports.
Block code references to dangerous runtime globals, including Bun, process, globalThis, global, self, Reflect, eval, Function, fetch, WebSocket, XMLHttpRequest, and require, except when used only as property keys.
Reject .constructor access on any receiver in dotted, computed-literal, and destructuring binding-pattern forms, including renamed, computed-string, and shorthand keys.
Use staticPropName() or equivalent logic to match both dotted properties (o.name) and computed literal properties (o["name"]).
Keep first-party and imported rule scanning converged: scanImportedRuleSource() should delegate to scanRuleSource() unless a genuinely imported-only restriction is required.
Run a raw-source integrity scan before transpilation, rejecting bidi controls, directional marks, and zero-width or invisible characters while permitting a leading BOM.
Do not use raw-text searches to detect dangerous identifiers or module names; use AST analysis for semantic checks.
Spell blocked code points numerically, such as 0x202e, rather than as literal characters or \u escapes.

Files:

  • src/engine/rule-scanner.ts
🔇 Additional comments (1)
src/engine/rule-scanner.ts (1)

188-188: LGTM!

Also applies to: 210-219

Comment thread tests/engine/rule-scanner.test.ts
@rhuanbarreto

Copy link
Copy Markdown
Contributor

@hancrafted thanks for the PR. Please fix the changes pointed by CodeRabbit

CodeRabbit suggested a test asserting a banned-global violation for
`export { fetch as local };`, but that assertion is provably false:
Bun.Transpiler erases an export specifier that names no local binding
(the source becomes `export {}`), so no reference reaches the AST walk,
and naming the global in a specifier is not valid ESM.

Add the local-export case asserting the correct outcome (no violation)
with a comment noting the declaration-form tests are what guard the
`source: null` subtree against a schema regression.

Signed-off-by: hancrafted <hancrafted@users.noreply.github.com>
rhuanbarreto added a commit that referenced this pull request Jul 24, 2026
The exotic-literal fix and the earlier null-`source` fix (#493) are the
same class: a node `AstNodeSchema` rejects is dropped by `parseNode`
together with its whole subtree, so a payload underneath is never walked
and every blocking rule in clauses 1-6 runs on a tree it is no longer in.
ARCH-024 governed what the scanner blocks but never stated that the walk
must reach the node first.

Adds Decision clause 7 (the walk visits every node; the schema must not
reject a node over a field it does not read), a matching DO/DON'T, a Risks
entry with mitigation, and Manual Enforcement item 10 treating any
`AstNodeSchema` narrowing as security-relevant. Records the operational
gotchas (node-drop = silent scan gap, `z.unknown()` non-optional in Zod v4,
transpiler dead-code stripping) in agent memory.

Signed-off-by: Rhuan Barreto <rhuan@barreto.work>

@rhuanbarreto rhuanbarreto left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks a lot for the contribution! LGTM!

@rhuanbarreto
rhuanbarreto merged commit d07db03 into archgate:main Jul 24, 2026
14 checks passed
@archgatebot archgatebot Bot mentioned this pull request Jul 24, 2026
rhuanbarreto added a commit that referenced this pull request Jul 24, 2026
### The bug

An AST node that fails `AstNodeSchema.safeParse` is dropped by
`parseNode` **together with its entire subtree**, because the walk skips
any node that fails to parse (`if (child) walk(child)`). Anything
dangerous underneath that node therefore goes unscanned — a silent
false-negative that `archgate check` reports as a pass.

This is the same class of bug as the null-`source` fix in #493, which
patched one trigger. A `Literal`'s `value` is the schema's only *other*
leaf that can fail validation: `type` is always present on an ESTree
node, and every other typed field (`source`, `object`, `property`,
`callee`, `left`) recurses back into the schema, bottoming out at
`value`.

Meriyah emits two value shapes the narrow union rejected:

- a plain **object** for a `RegExpLiteral` (`value: {}`)
- a **`bigint`** for `123n`

So a payload reached *off* such a literal escaped the walk entirely:

- `/x/.constructor.constructor` — the Function constructor (= `eval`)
off a RegExp receiver
- `(123n).constructor.constructor` — same, off a bigint receiver
- a banned global or dynamic import sitting beside the literal (`/x/ +
fetch("…")`, `[/x/, import("node:child_process")]`)

Naming `Function`/`eval` directly is already blocked, but the
`.constructor` chain is the property-route around that — and it was
slipping through whenever its receiver was one of these literals.

### The fix

Widen `value` to `z.unknown().optional()` (and `AstNode.value?:
unknown`). `value` is only ever *read* through `typeof … === "string"`
guards (`staticPropName`, `checkModuleSpecifier`), so the scanner never
consumes its shape — there is no reason to reject a node over it. No
ESTree node can now fail validation, so none is silently dropped from
the walk.

### Tests

New escape regressions in `rule-scanner-escapes.test.ts`:

- Function-constructor chain off a RegExp literal — unscanned before,
caught now
- Function-constructor chain off a bigint literal — unscanned before,
caught now
- banned global / dynamic import / `import.meta.require` beside a RegExp
or bigint literal
- positive controls: a clean RegExp literal and a clean bigint literal
still pass (the fix keeps the node in the walk, it does not start
flagging the literal)

One case is deliberately *not* tested as a hole: `/x/ || import(…)` is
stripped by the transpiler as provably-dead code (a RegExp literal is
always truthy), so the import genuinely never runs and correctly is not
scanned. The tests use reachable positions only, with that reasoning
documented inline.

`bun run validate` passes locally: oxlint, `tsc --build`, `oxfmt
--check`, full test suite, `archgate check` 44/44, compile.

The commit is DCO signed-off.

### Governance

Extends **ARCH-024 (Rule File Sandbox Boundary)** with a new Decision
clause (7) codifying the root class both this fix and #493 belong to: a
node rejected by `AstNodeSchema` is dropped by `parseNode` *with its
entire subtree*, so the (correct) blocking rules in clauses 1–6 never
run on the payload. The clause requires the walk to reach every node and
the schema to reject nothing a valid ESTree node can contain. Adds a
matching DO/DON'T, a Risk + mitigation, and a Manual Enforcement item
treating any `AstNodeSchema` narrowing as security-relevant.

---------

Signed-off-by: Rhuan Barreto <rhuan@barreto.work>
rhuanbarreto pushed a commit that referenced this pull request Jul 26, 2026
# archgate

## [0.51.0](v0.50.0...v0.51.0)
(2026-07-26)

### Features

* **adrs:** enforce concise, forward-only code comments (GEN-004)
([#496](#496))
([9a114b3](9a114b3)),
references [#2123](https://github.com/archgate/cli/issues/2123)
* **adrs:** flag stray files at the repository root (GEN-005)
([#535](#535))
([6a6e765](6a6e765)),
closes [#514](#514), references
[#500](#500)
* **engine:** add ctx.readYAML and ctx.checkCase rule helpers
([#497](#497))
([c5d82c5](c5d82c5)),
closes [#490](#490), references
[#490](#490)
[#491](#491)
[#499](#499)
[#499](#499)
[#499](#499)
[#499](#499)
* report truncated ADR briefings, trim the ADR corpus 15.6%, add GEN-005
briefing budget ([#501](#501))
([a9dab40](a9dab40))

### Bug Fixes

* **docs:** relocate ADR content to clear briefing-budget warnings
([#531](#531))
([c7419b3](c7419b3))
* **docs:** restore pt-br diacritics and enforce locale content
integrity ([#523](#523))
([db39104](db39104)),
closes [#516](#516), references
[#231](#231)
* **engine:** allow symlinks that resolve inside the project root
([#500](#500))
([387bf15](387bf15))
* **engine:** reject rule-file reads through a symlinked ancestor
directory ([#499](#499))
([a555f9d](a555f9d)),
references [#497](#497)
[#491](#491)
[#497](#497)
[#497](#497)
[#497](#497)
[#497](#497)
* **engine:** scan top-level export declarations with a null source
([#493](#493))
([d07db03](d07db03)),
closes [#491](#491)
* **engine:** stop dropping AST nodes with exotic literal values
([#494](#494))
([0015542](0015542)),
closes [#493](#493)
[#493](#493)
* **lint:** resolve no-bare-env-restore by captured key and lexical
scope ([#524](#524))
([7094a3a](7094a3a)),
closes [#498](#498)
* **rules:** make ARCH-020 and ARCH-023 match ctx.ast() instead of raw
text ([#533](#533))
([ad5529b](ad5529b)),
closes [#513](#513), references
[#486](#486)
* **tests:** replace bun:test anti-patterns with idiomatic patterns
([#512](#512))
([bcb086f](bcb086f))

---
This PR was generated with
[simple-release](https://github.com/TrigenSoftware/simple-release).

<details>
<summary>📄 Cheatsheet</summary>
<br>



You can configure the bot's behavior through a pull request comment
using the `!simple-release/set-options` command.

### Command Format

````md
!simple-release/set-options

```json
{
  "bump": {},
  "publish": {}
}
```
````

### Useful Parameters

#### Bump

| Parameter | Type | Description |
|-----------|------|-------------|
| `version` | `string` | Force set specific version |
| `as` | `'major' \| 'minor' \| 'patch' \| 'prerelease'` | Release type
|
| `prerelease` | `string` | Pre-release identifier (e.g., "alpha",
"beta") |
| `firstRelease` | `boolean` | Whether this is the first release |
| `skip` | `boolean` | Skip version bump |
| `byProject` | `Record<string, object>` | Per-project bump options for
monorepos |

#### Publish

| Parameter | Type | Description |
|-----------|------|-------------|
| `skip` | `boolean` | Skip publishing |
| `access` | `'public' \| 'restricted'` | Package access level |
| `tag` | `string` | Tag for npm publication |

### Usage Examples

#### Force specific version

````md
!simple-release/set-options

```json
{
  "bump": {
    "version": "2.0.0"
  }
}
```
````

#### Force major bump

````md
!simple-release/set-options

```json
{
  "bump": {
    "as": "major"
  }
}
```
````

#### Create alpha pre-release

````md
!simple-release/set-options

```json
{
  "bump": {
    "prerelease": "alpha"
  }
}
```
````

#### Publish with specific access and tag

````md
!simple-release/set-options

```json
{
  "bump": {
    "prerelease": "beta"
  },
  "publish": {
    "access": "public",
    "tag": "beta"
  }
}
```
````

### Custom Changelog Preamble

You can add custom markdown to the top of the changelog (right after the
version header) using the `!simple-release/set-preamble` command. The
markdown after the command line becomes the preamble.

```md
!simple-release/set-preamble

## What's new?

- The website was completely redesigned
- The new API gives you awesome possibilities
```

In a monorepo, pass the full package name after the command to target a
single package's changelog. Wrap the name in backticks so GitHub keeps
it as text instead of a mention:

```md
!simple-release/set-preamble `@your-org/core`

## Core changes

- New plugin system
```

Use one comment per package, plus one without a name for the whole
release.

### Access Restrictions

The commands can only be used by users with permissions:
- repository owner
- organization member
- collaborator

### Notes

- The last comment with `!simple-release/set-options` command takes
priority
- The last `!simple-release/set-preamble` comment per package takes
priority
- JSON must be valid, otherwise the `set-options` command will be
ignored
- Parameters apply only to the current release execution
- The commands can be updated by editing the comment or adding a new one


</details>

<!--
  Please do not edit this comment.
  simple-release-pull-request: true
  simple-release-branch-from: release
  simple-release-branch-to: main
-->

Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
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