Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 changelog.d/152.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Source-policy gates now scan JavaScript and TypeScript module source extensions under `src/`, so bundled `.js`, `.jsx`, `.mjs`, `.cjs`, `.tsx`, `.mts`, and `.cts` files no longer bypass the shared policy scanner.
8 changes: 4 additions & 4 deletions changelog.d/46.added.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ authored source tree on every `pnpm test` / CI run and fails the
build on any violation:
- PUL-Q007: no `eval`, `new Function`, `Function(...)` calls, or
dynamic `import()` of remote URLs / non-static specifiers in
`src/**/*.ts`.
- PUL-A001: no direct `gsap` imports from `src/scenes/**/*.ts`.
source modules under `src/`.
- PUL-A001: no direct `gsap` imports from source modules under `src/scenes/`.
- PUL-A002: no direct `howler` imports and no
`new HTMLAudioElement()` / `new Audio()` constructions in
`src/scenes/**/*.ts`.
source modules under `src/scenes/`.
- PUL-A003: no PixiJS / Three.js / Phaser imports in the runtime-core
file set.
- PUL-A004: no Remotion or video-rendering-library imports in the
runtime-core file set.
- PUL-A005: every `CompositionManifest`-typed export under
`src/compositions/**/*.ts` is a static array literal of
source modules under `src/compositions/` is a static array literal of
string-literal scene ids; top-level imperative-dispatch shapes are
forbidden.
- PUL-A006: no reveal.js / Spectacle imports in the runtime-core
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ runtime validator, loader hook, or bundle audit.

## Boundary

- Scan authored scene source under `src/scenes/**/*.ts` for direct
- Scan authored scene source modules under `src/scenes/` for direct
imports or dynamic imports of `gsap` and GSAP subpaths.
- Treat `src/runtime/timeline.ts` as the canonical GSAP boundary. It may
import `gsap`, exposes `createTimelineEngine()`, validates returned
Expand Down Expand Up @@ -76,7 +76,7 @@ Implementation must build on these incumbents:
The seam is a parameterized forbidden-import policy table in the shared
source scanner. A001 contributes a rule shaped like:

- scope: `src/scenes/**/*.ts`;
- scope: source modules under `src/scenes/`;
- forbidden module specifiers: `gsap` and `gsap/*`;
- allowed production boundary: `src/runtime/timeline.ts`;
- exemption tag: `PUL-A001-allow`.
Expand Down
16 changes: 8 additions & 8 deletions docs/design/pul-a002-a006-import-bans-preflight.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ The PUL-A001 preflight authorises this inheritance explicitly:

| Req | Scope (file set) | Forbidden specifiers | Allowed boundary | Exemption tag |
|-----|------------------|----------------------|------------------|---------------|
| PUL-A002 | `src/scenes/**/*.ts` | `howler`, `howler/*` (+ `new Audio()` / `new HTMLAudioElement()` value-position) | `src/runtime/audio.ts` (out of scope) | `PUL-A002-allow` |
| PUL-A003 | `src/**/*.ts` minus `src/scenes/**` (runtime-core file set) | `pixi.js`, `pixi.js/*`, `three`, `three/*`, `phaser`, `phaser/*` | scene-local imports under `src/scenes/**` | `PUL-A003-allow` |
| PUL-A004 | `src/**/*.ts` minus `src/scenes/**` (runtime-core file set) | `remotion`, `remotion/*`, `@remotion/*` | export pipeline (separate codebase, ADR-006) | `PUL-A004-allow` |
| PUL-A005 | `src/compositions/**/*.ts` | (special: declarative-manifest shape; see below) | n/a | `PUL-A005-allow` |
| PUL-A006 | `src/**/*.ts` minus `src/scenes/**` (runtime-core file set) | `reveal.js`, `reveal.js/*`, `spectacle`, `spectacle/*`, `@spectacle/*` | companion projects (separate, ADR-001) | `PUL-A006-allow` |
| PUL-A002 | source modules under `src/scenes/` | `howler`, `howler/*` (+ `new Audio()` / `new HTMLAudioElement()` value-position) | `src/runtime/audio.ts` (out of scope) | `PUL-A002-allow` |
| PUL-A003 | source modules under `src/` minus `src/scenes/**` (runtime-core file set) | `pixi.js`, `pixi.js/*`, `three`, `three/*`, `phaser`, `phaser/*` | scene-local imports under `src/scenes/**` | `PUL-A003-allow` |
| PUL-A004 | source modules under `src/` minus `src/scenes/**` (runtime-core file set) | `remotion`, `remotion/*`, `@remotion/*` | export pipeline (separate codebase, ADR-006) | `PUL-A004-allow` |
| PUL-A005 | source modules under `src/compositions/` | (special: declarative-manifest shape; see below) | n/a | `PUL-A005-allow` |
| PUL-A006 | source modules under `src/` minus `src/scenes/**` (runtime-core file set) | `reveal.js`, `reveal.js/*`, `spectacle`, `spectacle/*`, `@spectacle/*` | companion projects (separate, ADR-001) | `PUL-A006-allow` |

The "runtime-core file set" is computed at scan time as
`walkTsFiles(SRC_ROOT)` filtered to exclude `src/scenes/`. This makes
`walkSourceFiles(SRC_ROOT)` filtered to exclude `src/scenes/`. This makes
the scope self-extending: a new top-level runtime module (e.g.,
`src/feature-flags.ts`) is picked up automatically.

Expand All @@ -39,7 +39,7 @@ the scope self-extending: a new top-level runtime module (e.g.,
Each test file MUST build on these incumbents (defined in
`tests/runtime/source-policy.ts`):

- `walkTsFiles(root, excludes?)` — the file walker.
- `walkSourceFiles(root, excludes?)` — the file walker.
- `parseSource(text, file)` — TypeScript `SourceFile` factory with
parent pointers populated.
- `collectLineExemptions(sourceFile, allowTag)` — line-scoped
Expand Down Expand Up @@ -73,7 +73,7 @@ Each policy MUST:

PUL-A005 is not an import ban; it is a structural-shape requirement
on every exported `CompositionManifest`-typed binding under
`src/compositions/**/*.ts`. The detection rule is two-phase:
source modules under `src/compositions/`. The detection rule is two-phase:

1. **Top-level statement shape.** A composition module's top-level
statements MUST be import declarations, export declarations, type
Expand Down
4 changes: 2 additions & 2 deletions docs/design/pul-q003-url-state-determinism-preflight.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ workflow layer.
`effectiveMode(target)` for each navigation and builds fresh
per-navigation context.
- Source-policy enforcement belongs in a Vitest static policy over
authored `src/**/*.ts`, using `tests/runtime/source-policy.ts` and
authored source modules under `src/`, using `tests/runtime/source-policy.ts` and
the screenshot-determinism source scan precedent. Do not add a
browser runtime validator for persisted-state targeting.

Expand Down Expand Up @@ -76,7 +76,7 @@ Implementation must build on these incumbents:
| Mode dispatch | Mode is derived with `effectiveMode(target)` for each navigation. Omitted `mode` selects fresh `present`; it must not reuse a previous mode from memory or storage. |
| Scene context | `ctx.mode` is a derived hint from the current target only. Scenes may branch on `ctx.mode`; they must not parse query strings or read storage/cookies/history to determine target or mode. |
| Runtime validation | `validateRuntime()` stays graph-shape validation. Q003 enforcement is source-policy plus existing URL/parser/loader tests, not scene metadata validation. |
| Source policy gate | Add or extend a Vitest policy scan over `src/**/*.ts`. Reuse `source-policy.ts`; do not create regex-only scans or duplicate walkers. Any exemption must be line-scoped and reasoned, e.g. `PUL-Q003-allow: <reason>`, and must not apply to target selection. |
| Source policy gate | Add or extend a Vitest policy scan across source modules under `src/`. Reuse `source-policy.ts`; do not create regex-only scans or duplicate walkers. Any exemption must be line-scoped and reasoned, e.g. `PUL-Q003-allow: <reason>`, and must not apply to target selection. |
| Auth, secrets, and env binding | Target selection needs no auth, secrets, env vars, `.env`, or host config. `process.env`, `import.meta.env`, and `process.argv` must not determine scene, beat, composition, or mode. |
| OS/process exposure | Do not pass target state, secret-bearing URLs, cookies, or env-derived values through shell argv. Tests should run in-process under Vitest and report relative path, line, label, and trimmed line text only. |
| Error envelope | Navigation failures use existing `navigation grammar is invalid:`, `scene navigation failed:`, `composition resolution failed:`, and `data-pulsar-navigation-error` surfaces. Diagnostics may name ids, modes, indexes, and bounded messages; never dump cookies, headers, env, argv, raw scene objects, or full credential-bearing URLs. |
Expand Down
4 changes: 2 additions & 2 deletions docs/design/pul-q007-runtime-code-execution-preflight.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ Date: 2026-05-12

PUL-Q007 is a runtime-source security policy: published runtime code
must not execute code that is not already present in the bundle. The
right enforcement is a Vitest static-policy suite over `src/**/*.ts`,
using the TypeScript AST scanner precedent from
right enforcement is a Vitest static-policy suite that scans source
modules under `src/`, using the TypeScript AST scanner precedent from
`tests/runtime/screenshot-determinism-source.test.ts` and the CI-gate
precedent from `tests/runtime/workbench-graph.test.ts`.

Expand Down
4 changes: 2 additions & 2 deletions docs/design/pul-q008-dom-css-accessibility-preflight.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ Implementation must build on these incumbents:
`ctx.stage.ownerDocument.createElement(...)`, scene-local
`appendChild`, and lifecycle cleanup through `cleanup(ctx)`.
- Existing DOM bypass policy: PUL-Q004's source scan over
`src/scenes/**/*.ts`, especially the bans on ambient `document`
source modules under `src/scenes/`, especially the bans on ambient `document`
attachment roots, global listeners, observers, and DOM prototype
monkey-patches.
- Source-policy helpers: `walkTsFiles`, `parseSource`,
- Source-policy helpers: `walkSourceFiles`, `parseSource`,
`collectLineExemptions`, `lineText`, access-path helpers, and
bounded `{ file, line, text, label }` diagnostics from
`tests/runtime/source-policy.ts`.
Expand Down
2 changes: 1 addition & 1 deletion docs/scene-trust-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ one.
[`tests/runtime/policy-q007-remote-code-execution.test.ts`](../tests/runtime/policy-q007-remote-code-execution.test.ts)
bans `eval`, `new Function(...)`, `Function(...)`, and dynamic
`import(specifier)` whose specifier is a remote URL or non-static
expression in authored runtime source (`src/**/*.ts`). It catches
expression in authored runtime source (source modules under `src/`). It catches
attempts to execute code that is not present in the published
bundle. It does not transform a bundled scene module into
untrusted-safe code, and it does not restrict what bundled scene
Expand Down
2 changes: 1 addition & 1 deletion tests/runtime/navigation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -856,7 +856,7 @@ describe('PUL-Q003 — persisted browser state never determines the target', ()
// navigation event matches `parseNavigationSearch(location.search)`
// exactly and that `effectiveMode(...)` ignores the seeded state.
//
// The structural ban on these surfaces in `src/**/*.ts` is enforced
// The structural ban on these surfaces in source modules under `src/` is enforced
// separately by `policy-q003-url-state-determinism.test.ts`. This
// block adds black-box coverage: if a future refactor of
// `subscribeNavigation` / `bootstrapNavigation` / `effectiveMode`
Expand Down
16 changes: 8 additions & 8 deletions tests/runtime/policy-a001-timeline-encapsulation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
collectLineExemptions,
parseSource,
scanImportSpecifiers,
walkTsFiles,
walkSourceFiles,
} from './source-policy';

// PUL-A001 — Timeline library encapsulation.
Expand All @@ -19,7 +19,7 @@ import {
// directly. Scene timelines SHALL be constructed via the timeline
// utilities exposed on the scene context."
//
// Enforcement: a Vitest source scan over `src/scenes/**/*.ts` that
// Enforcement: a Vitest source scan across source modules under `src/scenes/` that
// flags every import — static (`import ... from 'gsap'`), dynamic
// (`import('gsap')`), and type-only (`import type ... from 'gsap'`) —
// of the `gsap` package and its subpaths. The runtime adapter at
Expand Down Expand Up @@ -186,13 +186,13 @@ describe('PUL-A001 — timeline library encapsulation (source scan)', () => {
});

describe('runtime tree (current code revision)', () => {
it('scenes root `src/scenes/` exists and contains at least one .ts file', () => {
it('scenes root `src/scenes/` exists and contains at least one source module file', () => {
expect(statSync(SCENES_ROOT).isDirectory()).toBe(true);
expect(walkTsFiles(SCENES_ROOT).length).toBeGreaterThan(0);
expect(walkSourceFiles(SCENES_ROOT).length).toBeGreaterThan(0);
});

it('contains no A001 violations across `src/scenes/**/*.ts`', () => {
const files = walkTsFiles(SCENES_ROOT);
it('contains no A001 violations across source modules under `src/scenes/`', () => {
const files = walkSourceFiles(SCENES_ROOT);
const findings: SourceFinding[] = [];
for (const file of files) {
const text = readFileSync(file, 'utf-8');
Expand All @@ -209,15 +209,15 @@ describe('PUL-A001 — timeline library encapsulation (source scan)', () => {
});

it('runtime adapter `src/runtime/timeline.ts` is exempt by scope (the boundary)', () => {
// The scope is `src/scenes/**/*.ts`, so the adapter never enters
// The scope is source modules under `src/scenes/`, so the adapter never enters
// the scan — it would never be flagged even though it imports
// `gsap`. This test pins that property explicitly so a future
// change to the scope cannot silently drag the adapter in.
const adapter = join(SRC_ROOT, 'runtime', 'timeline.ts');
expect(statSync(adapter).isFile()).toBe(true);
const text = readFileSync(adapter, 'utf-8');
expect(text).toMatch(/from\s+['"]gsap['"]/);
const sceneFiles = walkTsFiles(SCENES_ROOT);
const sceneFiles = walkSourceFiles(SCENES_ROOT);
expect(sceneFiles).not.toContain(adapter);
});
});
Expand Down
14 changes: 7 additions & 7 deletions tests/runtime/policy-a002-audio-encapsulation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
parseSource,
pathResolvesTo,
scanImportSpecifiers,
walkTsFiles,
walkSourceFiles,
} from './source-policy';

// PUL-A002 — Audio library encapsulation.
Expand All @@ -27,7 +27,7 @@ import {
// where a scene drops down to raw Web Audio with a documented
// justification and registers cleanup with the runtime."
//
// Enforcement: a Vitest source scan over `src/scenes/**/*.ts` with
// Enforcement: a Vitest source scan across source modules under `src/scenes/` with
// two checks:
//
// 1. Direct imports of `howler` (or any `howler/*` subpath) are
Expand Down Expand Up @@ -371,13 +371,13 @@ describe('PUL-A002 — audio library encapsulation (source scan)', () => {
});

describe('runtime tree (current code revision)', () => {
it('scenes root `src/scenes/` exists and contains at least one .ts file', () => {
it('scenes root `src/scenes/` exists and contains at least one source module file', () => {
expect(statSync(SCENES_ROOT).isDirectory()).toBe(true);
expect(walkTsFiles(SCENES_ROOT).length).toBeGreaterThan(0);
expect(walkSourceFiles(SCENES_ROOT).length).toBeGreaterThan(0);
});

it('contains no A002 violations across `src/scenes/**/*.ts`', () => {
const files = walkTsFiles(SCENES_ROOT);
it('contains no A002 violations across source modules under `src/scenes/`', () => {
const files = walkSourceFiles(SCENES_ROOT);
const findings: SourceFinding[] = [];
for (const file of files) {
const text = readFileSync(file, 'utf-8');
Expand All @@ -401,7 +401,7 @@ describe('PUL-A002 — audio library encapsulation (source scan)', () => {
expect(statSync(adapter).isFile()).toBe(true);
const text = readFileSync(adapter, 'utf-8');
expect(text).toMatch(/from\s+['"]howler['"]/);
const sceneFiles = walkTsFiles(SCENES_ROOT);
const sceneFiles = walkSourceFiles(SCENES_ROOT);
expect(sceneFiles).not.toContain(adapter);
});
});
Expand Down
8 changes: 4 additions & 4 deletions tests/runtime/policy-a003-rendering-libraries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
collectLineExemptions,
parseSource,
scanImportSpecifiers,
walkTsFiles,
walkSourceFiles,
} from './source-policy';

// PUL-A003 — Optional rendering libraries are scene-local.
Expand All @@ -21,7 +21,7 @@ import {
// Enforcement: a Vitest source scan over the runtime-core file set
// (everything under `src/` EXCEPT `src/scenes/**`) that flags any
// import of `pixi.js`, `three`, or `phaser` (each with subpath
// wildcards). Scene files under `src/scenes/**/*.ts` may adopt these
// wildcards). Scene source modules under `src/scenes/` may adopt these
// libraries locally; they are out of scope by construction.
//
// The runtime-core boundary covers `src/runtime/**`, `src/compositions/**`,
Expand All @@ -39,13 +39,13 @@ const RULE: ImportBanRule = {
const SCENES_ROOT = join(SRC_ROOT, 'scenes');

/**
* Runtime-core file set: every `.ts` under `src/` that is NOT under
* Runtime-core file set: every source module under `src/` that is NOT under
* `src/scenes/`. Computed at scan time so any future top-level file
* under `src/` (e.g., a new `src/feature-flags.ts`) is automatically
* included without editing the test.
*/
function runtimeCoreFiles(): readonly string[] {
return walkTsFiles(SRC_ROOT).filter((file) => !file.startsWith(`${SCENES_ROOT}/`));
return walkSourceFiles(SRC_ROOT).filter((file) => !file.startsWith(`${SCENES_ROOT}/`));
}

function scanForA003(source: string, file: string): readonly SourceFinding[] {
Expand Down
4 changes: 2 additions & 2 deletions tests/runtime/policy-a004-export-pipeline.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
collectLineExemptions,
parseSource,
scanImportSpecifiers,
walkTsFiles,
walkSourceFiles,
} from './source-policy';

// PUL-A004 — Live runtime is independent of the export pipeline.
Expand All @@ -37,7 +37,7 @@ const RULE: ImportBanRule = {
const SCENES_ROOT = join(SRC_ROOT, 'scenes');

function runtimeCoreFiles(): readonly string[] {
return walkTsFiles(SRC_ROOT).filter((file) => !file.startsWith(`${SCENES_ROOT}/`));
return walkSourceFiles(SRC_ROOT).filter((file) => !file.startsWith(`${SCENES_ROOT}/`));
}

function scanForA004(source: string, file: string): readonly SourceFinding[] {
Expand Down
14 changes: 7 additions & 7 deletions tests/runtime/policy-a005-declarative-composition.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
collectLineExemptions,
lineText,
parseSource,
walkTsFiles,
walkSourceFiles,
} from './source-policy';

// PUL-A005 — Composition is declarative.
Expand All @@ -20,7 +20,7 @@ import {
// (e.g., `if/else` branching or position-based dispatch in a control
// script) as the source of truth for composition order."
//
// Enforcement: a Vitest source scan over `src/compositions/**/*.ts`
// Enforcement: a Vitest source scan across source modules under `src/compositions/`
// with two checks:
//
// 1. Every exported `const X: CompositionManifest = <init>` must
Expand Down Expand Up @@ -147,7 +147,7 @@ function scanForA005(source: string, file: string): readonly SourceFinding[] {
// A composition module's default export is implicitly the
// composition manifest. Without a type assertion there's no
// declared annotation to read, but the file is still under
// `src/compositions/**/*.ts` and the export still becomes
// source modules under `src/compositions/` and the export still becomes
// the registered manifest. We enforce the same static-array
// rule so `export default buildManifest();` is caught.
// f) `export const m = <init>` (untyped, no assertion) AND
Expand Down Expand Up @@ -798,13 +798,13 @@ describe('PUL-A005 — composition is declarative (source scan)', () => {
});

describe('runtime tree (current code revision)', () => {
it('compositions root `src/compositions/` exists and contains at least one .ts file', () => {
it('compositions root `src/compositions/` exists and contains at least one source module file', () => {
expect(statSync(COMPOSITIONS_ROOT).isDirectory()).toBe(true);
expect(walkTsFiles(COMPOSITIONS_ROOT).length).toBeGreaterThan(0);
expect(walkSourceFiles(COMPOSITIONS_ROOT).length).toBeGreaterThan(0);
});

it('contains no A005 violations across `src/compositions/**/*.ts`', () => {
const files = walkTsFiles(COMPOSITIONS_ROOT);
it('contains no A005 violations across source modules under `src/compositions/`', () => {
const files = walkSourceFiles(COMPOSITIONS_ROOT);
const findings: SourceFinding[] = [];
for (const file of files) {
const text = readFileSync(file, 'utf-8');
Expand Down
4 changes: 2 additions & 2 deletions tests/runtime/policy-a006-slide-frameworks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
collectLineExemptions,
parseSource,
scanImportSpecifiers,
walkTsFiles,
walkSourceFiles,
} from './source-policy';

// PUL-A006 — Live runtime is independent of slide frameworks.
Expand All @@ -34,7 +34,7 @@ const RULE: ImportBanRule = {
const SCENES_ROOT = join(SRC_ROOT, 'scenes');

function runtimeCoreFiles(): readonly string[] {
return walkTsFiles(SRC_ROOT).filter((file) => !file.startsWith(`${SCENES_ROOT}/`));
return walkSourceFiles(SRC_ROOT).filter((file) => !file.startsWith(`${SCENES_ROOT}/`));
}

function scanForA006(source: string, file: string): readonly SourceFinding[] {
Expand Down
Loading
Loading