Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
c3f1220
Preserve audio beds in head slices
Brad-Edwards May 23, 2026
829ca51
Merge pull request #154 from KeplerOps/147-preserve-audio-beds
Brad-Edwards May 23, 2026
73a5676
Apply asset policy to audio beds
Brad-Edwards May 23, 2026
0674457
Merge pull request #155 from KeplerOps/148-bed-url-policy
Brad-Edwards May 23, 2026
a2ca472
Harden titleSlam data text rendering
Brad-Edwards May 24, 2026
d8c76e2
Merge pull request #156 from KeplerOps/149-title-slam-escape
Brad-Edwards May 24, 2026
550f681
fix: harden prompter popout URLs
Brad-Edwards May 24, 2026
3ba7e2d
Merge pull request #157 from KeplerOps/150-prompter-popout-url
Brad-Edwards May 24, 2026
8d28c60
fix: scope presenter bridge sessions
Brad-Edwards May 24, 2026
608d51d
Merge pull request #158 from KeplerOps/151-presenter-session-scope
Brad-Edwards May 24, 2026
89dbd38
Expand source policy extension coverage
Brad-Edwards May 24, 2026
81ea11a
Merge pull request #159 from KeplerOps/152-source-policy-extensions
Brad-Edwards May 24, 2026
459c8bb
attempt sequence fixes
Brad-Edwards May 30, 2026
78338ab
Add defineScene authoring normalizer as single source of scene defaul…
Brad-Edwards May 30, 2026
4d5d2d3
Centralize per-mode behavior into a ModeProfile data table
Brad-Edwards May 30, 2026
d22bbb7
refactor(scene-loader): decompose buildLoad/runTarget below the compl…
Brad-Edwards May 30, 2026
9a26f11
refactor(timeline): extract presenter transport into an opt-in presen…
Brad-Edwards May 30, 2026
976a892
refactor(audio): close audio.ts cognitive-complexity suppressions
Brad-Edwards May 30, 2026
4f1b4fc
refactor(audio): single disposal gate, shared bed registration, table…
Brad-Edwards May 30, 2026
3cf332c
refactor(audio): fold sprite-map validation into assertSoundDefinition
Brad-Edwards May 30, 2026
05cd30f
refactor(audio): drop bespoke bed routing, collapse output policy to …
Brad-Edwards May 30, 2026
edb4bd6
refactor(runtime): simplify composition-resolver and scene-navigation…
Brad-Edwards May 30, 2026
daa0762
refactor(runtime): extract withFailureIsolation decorator over bare l…
Brad-Edwards May 30, 2026
d4b28d4
refactor(templates): author L2 templates against real DOM types
Brad-Edwards May 30, 2026
4b8c88f
test: relocate source-policy suite into a dedicated blocking gate
Brad-Edwards May 31, 2026
f1e90cd
refactor(audio): collapse AudioError subclasses to one categorized er…
Brad-Edwards May 31, 2026
635d80a
test: harden validator-fuzz field oracle and pin AudioError 'option' …
Brad-Edwards May 31, 2026
a9306c4
docs: drop stale complexity-gate narration from runtime comments
Brad-Edwards May 31, 2026
91f39c1
docs(runtime): collapse narrative comments to terse contracts
Brad-Edwards May 31, 2026
14ea31e
refactor(runtime): inline linter-noise helpers and collapse condition…
Brad-Edwards May 31, 2026
6e31da3
refactor(runtime): simplify audio/timeline patterns
Brad-Edwards May 31, 2026
b29e1f6
refactor: remove root DOM casts and trim system/deck boilerplate
Brad-Edwards May 31, 2026
f4f67b2
test: consolidate duplicated test fakes into tests/support/fakes.ts
Brad-Edwards May 31, 2026
8127170
test: cover the L2 template library + timing helpers; scope decks out…
Brad-Edwards May 31, 2026
684bd62
ci(sonar): exclude example decks from duplication detection
Brad-Edwards May 31, 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
26 changes: 23 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ jobs:
run: pnpm install --frozen-lockfile
- name: Install pre-commit
run: pip install pre-commit
# typecheck and vitest hooks have dedicated jobs below that also
# upload coverage. Skip them here to avoid duplicate runs.
# typecheck, vitest, and policy hooks have dedicated jobs below.
# Skip them here to avoid duplicate runs.
- name: Run pre-commit
run: pre-commit run --all-files --show-diff-on-failure
env:
SKIP: typecheck,vitest
SKIP: typecheck,vitest,policy

typecheck:
name: Typecheck
Expand Down Expand Up @@ -89,6 +89,26 @@ jobs:
path: coverage/
retention-days: 7

policy:
# Structural code-shape audits (PUL-Q001/Q003/Q007, PUL-A001..A010,
# complexity-gate allowlist). Relocated out of the behavior suite
# into their own vitest project so their whole-tree AST scans no
# longer starve under parallel load — enforcement is unchanged and
# this step is blocking. Mirrored by the `policy` pre-commit hook.
name: Policy gate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v5
with:
version: 9.15.0
- uses: actions/setup-node@v5
with:
node-version: '22'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm policy

build:
name: Build
runs-on: ubuntu-latest
Expand Down
11 changes: 11 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,14 @@ repos:
language: system
pass_filenames: false
files: ^(src/.*\.ts|tests/.*\.ts|vitest\.config\.ts|package\.json)$

# Structural code-shape audits, relocated out of the behavior
# suite into their own vitest project (vitest.policy.config.ts).
# Blocking — same enforcement set as before, just no longer mixed
# into `pnpm test`. Mirrored by the CI `policy` job.
- id: policy
name: pnpm policy (source-policy gate)
entry: bash -c 'pnpm policy'
language: system
pass_filenames: false
files: ^(src/.*\.ts|tests/.*\.ts|biome\.json|vitest\.policy\.config\.ts|package\.json)$
20 changes: 20 additions & 0 deletions changelog.d/+aces-pulsar-decks.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Rebuilt both reference decks with bespoke per-deck CSS rather than the L2
template kit. `pulsar-intro` is now an editorial, two-surface deck
(near-black + paper) demonstrating the runtime to a presentation author
who has never seen Pulsar; `aces-ecosystem-intro` is a standards-body
briefing register grounded in citations to `aces-sdl/` and the F1
literature review.

Added timeline-owned active segment reporting to the workbench. Presenter
scene navigation now moves by an explicit segment cursor; `ArrowRight`,
`PageDown`, `ArrowLeft`, and `PageUp` keep the visible scene and
`data-pulsar-scene-target` aligned without relying on GSAP callback replay
after seeks.

Extended the trailing tween on each deck's last scene so the composition
master never reaches its natural end. Advancing past the outro now holds
on the final scene with a clear `end · N of N` folio instead of tearing
all scenes down and showing a blank stage.

Made cinematic chrome atmosphere composition-opt-in and fixed presenter
session entropy on non-secure Tailscale HTTP origins.
28 changes: 28 additions & 0 deletions changelog.d/+audio-engine-slimming.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
Closed the three `src/runtime/audio.ts` cognitive-complexity
suppressions. `unlock()` now delegates its HTML5 and Web Audio
fallback branches to the `unlockHtml5Fallback` / `resumeWebAudioContext`
module helpers; `play()` delegates option validation to `validatePlay`
and per-instance engine output to `applyPlayToHandle`. The
`normalizeSources` offender was already covered by the hoisted
`normalizeAudioUrl`. Behavior is unchanged — the audio service public
methods, output policies, error families, composition bed routing, cue
gate, and master-mute semantics are byte-identical. The audio.ts rows
were removed from `docs/design/complexity-backlog.md` and the
complexity-gate policy oracle.

Slimmed the audio service internals without changing observable
behavior: the per-service `disposed` boolean and its scattered guards
were replaced by a single internal `AbortController` so the navigation
signal and an explicit `stopAll()` converge on one disposal gate and one
teardown; the composition bed is registered through the same
`registerSound` core scene sounds use under the reserved, non-kebab
`composition audio bed` id (the bespoke `startBed` arrow and `bedAllowed`
local are gone — the bed is now started inline through the shared core,
gated against its own declared `src`), keeping it unreachable from
`ctx.audio`; and the four per-option `assertPlayOption*` helpers were
folded into a table-driven `assertPlayOptions`. The validated
`outputPolicy` string is now collapsed once at construction into the two
orthogonal output axes the policy actually controls — `muted` (engine
mute) and `emitCues` (rehearsal cue sink) — so no service method
re-derives behavior from the policy string. The public `outputPolicy`
option and its allowlist validation are unchanged.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Collapsed the audio error hierarchy and de-duplicated the shape-validator tests. The nine `AudioError` subclasses
(`AudioSoundError` / `AudioGroupError` / `AudioRangeError` / `AudioSourceError`, etc.) are gone — no `src/` caller
discriminated them — replaced by one `AudioError` carrying a `category` discriminant (`sound` / `group` / `source` /
`range` / `option`) and module-private per-category constructors. `ctx.audio` runtime behavior, the throwable surface,
and every rejected input are unchanged. The brittle field-by-field `.each` validation loops for `assertSceneModule`,
`assertCompositionManifest`, and `assertAudioBedDeclaration` are replaced by one representative assert per shape plus a
shared seeded property fuzz (`tests/runtime/validator-fuzz.ts`) covering the same malformed-input classes (omission,
wrong type, out-of-range number, non-kebab id) and asserting the offending field is named.
14 changes: 14 additions & 0 deletions changelog.d/+authoring-real-dom.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
L2 template/scene authoring now uses real `lib.dom` types. The
structural fake-DOM types (`TemplateDomElement` / `TemplateDomFactory`
/ `TemplateStageElement`) and the per-call defensive ctx narrowing
(`isTemplateCtx` / `isStageShape` / `isGsapShape`) are gone; templates
take `HTMLElement` / `Document` directly and read `ctx` through one
`asTemplateCtx` view that checks only the genuine off-DOM
(`stage === null`) path. Every `as unknown as HTMLElement|Document`
cast in templates and decks is removed. Decks reference a shared
`TemplateTimeline` type instead of re-declaring a structural timeline
subset. The deck-only templates `operatorDossier`, `incidentPlate`,
and `haulCitations` moved into `src/decks/local-calgary-v2/templates/`
(with their CSS) since no other deck uses them. DOM-touching template
tests opt into `happy-dom` per file and assert against real rendering;
the runtime fake-stage suites stay node-env and unchanged.
8 changes: 8 additions & 0 deletions changelog.d/+comment-sweep.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Collapsed narrative/ceremony comments across the runtime to terse
one-line contracts. Trimmed module preambles, per-field JSDoc on
`WorkbenchSceneCtx` / `SceneLoaderOptions` / `LoadSceneNavigationTargetOptions`,
and review-cycle narration in `src/main.ts` and `src/runtime/{scene-loader,
scene-loader-ctx,scene-loader-guard,scene,navigation,scene-navigation,
composition-resolver,composition,validation,audio,timeline,presenter,
prompter,workbench-chrome}.ts`. Comments only — no code, behavior, or
public-API change.
23 changes: 23 additions & 0 deletions changelog.d/+composition-resolver-internals.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
Refactored the composition-resolver and scene-navigation internals
without changing behavior. `SceneActivation` identity is now embedded
in each plan step at build time (no per-call reconstruction); the
three finalizers collapse into one `finalize()` that selects
aggregate-vs-reraise from whether `onSceneFailed` was supplied;
`onSceneFailed` is wrapped once at lifecycle-context build; the bare
mount→compose→run→cleanup engine (`runLifecycle`) is separated from
the scene-failure-isolation concern, which now lives in a named
`withFailureIsolation` decorator (owning the failure bucket, the
once-wrapped `onSceneFailed`, and the aggregate-vs-reraise routing)
that `resolveComposition` is composed from; the three composition-
resolution paths (from-start / scene / index) unify behind one
parameterized index finder; and the resolver's run-option builder is
reduced to a single strip. The bare engine (`runLifecycle`,
`orchestrate` via `buildPlan` + `buildLifecycleContext`) is exported so
it can be driven directly in tests with a plain collecting
`reportFailure`. Added both-path (onSceneFailed supplied / omitted)
regression coverage asserting cleanup-exactly-once-per-activation and
correct error routing for create-throw, timeline-throw, cleanup-throw,
abort-mid-mount, and a repeated scene id where one occurrence fails,
plus a bare-engine suite that drives `runLifecycle` without the
decorator. Public signatures, `data-pulsar-*` attributes, and error
wording are unchanged.
10 changes: 10 additions & 0 deletions changelog.d/+mode-profile-table.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Centralized per-mode workbench behavior into a single `ModeProfile`
data table (`src/runtime/mode-profile.ts`). The audio output policy,
composition-slice truncation, chrome visibility, audio-bed suppression,
scrub cue gate, and head-scene runner hints were previously scattered
as `mode === X` branches across `scene-loader.ts` and
`workbench-chrome.ts`; they now read one frozen profile per mode.
Behavior is unchanged (snapshot-equivalence test), and collapsing the
four runner-hint ternaries into a single `...runnerHints` spread dropped
`runLifecycle` below the cognitive-complexity gate, closing a
complexity-backlog entry.
14 changes: 14 additions & 0 deletions changelog.d/+policy-gate-relocation.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Relocated the policy / source-scan suites
(`tests/runtime/policy-*.test.ts`,
`tests/runtime/screenshot-determinism-source.test.ts`, and their shared
`source-policy.ts` AST framework) out of the default behavior suite
into a dedicated, still-blocking gate. They run via a new `pnpm policy`
script against `vitest.policy.config.ts` (serial, generous timeout) and
are excluded from `vitest.config.ts`, so `pnpm test` is now
behavior-only and no longer flakes on the structural AST scans starving
under parallel load (PUL-Q003 / PUL-Q007 5s timeouts). Enforcement is
unchanged: `pnpm policy` is wired as a blocking job in
`.github/workflows/ci.yml` and a blocking hook in
`.pre-commit-config.yaml`, running the identical violation set. No
policy check was dropped or weakened; the Biome complexity-gate
override for the cluster is untouched.
10 changes: 10 additions & 0 deletions changelog.d/+presenter-transport-extraction.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Extracted the presenter transport machinery (advance / hold / skip /
pause / resume command translation) out of the always-on timeline
composition path into a dedicated opt-in module
(`src/runtime/presenter-transport.ts`). The transport is wired onto the
master timeline only when a navigation forwards a presenter controller
(`mode=present`); a non-present navigation never instantiates it.
`timeline.ts` keeps the GSAP composition spine — `composeMasterTimeline`,
the scene label namespace, `assertSceneTimeline`, and the `MasterBeat`
beat query. No public signatures, `data-pulsar-*` attributes, or
cross-engine timing behavior changed.
12 changes: 12 additions & 0 deletions changelog.d/+scene-loader-decomposition.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Decomposed the scene-loader god-functions (`buildLoad`, `runTarget`)
into cohesive single-responsibility units below the cognitive-complexity
gate, deleting both `noExcessiveCognitiveComplexity` suppressions.
Per-navigation audio service / presenter pipe / ctx factory moved to
`src/runtime/scene-loader-ctx.ts`; the present-mode audio unlock-gate
predicate and the composition chrome dispatch policy to
`src/runtime/scene-loader-guard.ts`. The `beat` / `mode` grammar rules
are now sourced from a single `NAVIGATION_GRAMMAR` object in
`src/runtime/navigation.ts`, consumed by both `parseNavigationSearch`
and the loader's defense-in-depth re-check (the forged-target trust
seam is retained). `createSceneLoader`, all exported types, the
`data-pulsar-*` stage attributes, and runtime behavior are unchanged.
13 changes: 13 additions & 0 deletions changelog.d/+simplify-audio-timeline.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Simplified the audio/timeline runtime cluster without behavior change:
inlined the single-use `validateSpeed` / `validateRepeat` validators
into `GsapMasterTimeline.setSpeed` / `.repeat`, rebuilt
`buildRunComposeOptions` as a single conditional-spread literal instead
of an empty object with four `as`-cast field assignments, and collapsed
the `...(x === undefined ? {} : { x })` idiom to `...(x && { x })` for
the object/boolean-typed `sprite` / `mute` fields in
`createHowlerAudioEngine`. Also trimmed the audio-unlock-dom module
preamble, the orphaned/duplicated adapter JSDoc, and the codex-cycle
narration to terse contracts. Public signatures (`AudioService` /
`AudioError` / `MasterTimeline` / `TimelineEngine` / the audio-unlock
adapter), error strings, `data-pulsar-*` attributes, and the abort-race
isolation guard are unchanged.
11 changes: 11 additions & 0 deletions changelog.d/+simplify-runtime-patterns.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Simplified the scene-loader and navigation/composition runtime cluster
without behavior change: inlined linter-noise micro-helpers
(`setStageAttr` / `clearStageAttr` / `audioOutputPolicyFor` /
`audioServiceOptions`), collapsed the `...(x === undefined ? {} : { x })`
conditional-spread idiom to its positive `...(x ? { x } : {})` form,
replaced writable-intermediate-then-freeze object construction in
`parseNavigationSearch` / `composeSegments` / `buildPrompterScript` with
direct frozen literals, and merged the two-stage `chromeBehavior`
extraction in `scene-loader-guard`. Public signatures, `data-pulsar-*`
attributes, error strings, and PUL-Q008 attribute-literal handling are
unchanged.
8 changes: 8 additions & 0 deletions changelog.d/+simplify-system-bootstrap.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Made `createDomWorkbenchChrome` and `createDomAudioUnlockAdapter` generic
over their concrete element type so `src/main.ts` mounts a real
`HTMLElement` / `HTMLButtonElement` without `as unknown as Node` casts.
Trimmed narrative/ceremony comments in the chrome, prompter-window,
practice-renderer, and keyboard-source modules, and collapsed the
repeated `tl.fromTo` reveal boilerplate in the pulsar-intro deck behind a
local `reveal` helper (byte-identical timeline output). Behavior, exported
signatures, DOM attributes, and tests are unchanged.
11 changes: 11 additions & 0 deletions changelog.d/+template-coverage.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Behavioral test coverage for the under-tested L2 template library and
runtime timing helpers. New mount-and-assert suites exercise the real
rendered DOM, authored timeline beats, and cleanup for `terminal`,
`card-carousel`, `activity-feed-payoff`, `chat-pick-list`,
`split-dialogue-email`, `split-pane-terminal-doc`, `metric-ticker`, the
`_shared` template envelope, the `register` token barrel, and the
abortable-timing primitives in `helpers/timing`. Product line coverage
(runtime + template library) rises from ~93% to ~99%; `terminal.ts`
alone goes 35% to 99%. The example decks (demonstration content,
exercised by the Playwright E2E) are scoped out of the coverage gate —
the gate measures the product, not the sample decks.
11 changes: 11 additions & 0 deletions changelog.d/+test-fakes-fixture.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Extracted the duplicated jsdom-free test fakes into a single
`tests/support/fakes.ts` fixture: the `mode=*` fixture-scene stage stub
(was re-derived byte-for-byte in five `tests/scenes/*-fixture.test.ts`
files), the synthetic `HTMLElement`/`Document` tree the chrome pack
tests use (was duplicated across `chrome-slots` and `chrome-extras`),
and the event-emitting / no-op presenter controllers (was re-rolled in
`helpers` and `presenter-driven`). Typing the chrome fake as the real
DOM interfaces dropped every `as unknown as HTMLElement|FakeElement`
cast at the chrome-test call sites, and trimmed the copy-pasted
coverage-narration headers to one line each. Assertions, test counts,
and behavior coverage are unchanged.
1 change: 1 addition & 0 deletions changelog.d/147.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Preserve composition audio beds in loop and scrub head-only composition modes.
1 change: 1 addition & 0 deletions changelog.d/148.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Apply configured asset URL policy to composition audio-bed validation and playback.
1 change: 1 addition & 0 deletions changelog.d/149.security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hardened the `titleSlam` template so title words are rendered through DOM text and `data-text` assignments instead of raw span HTML.
1 change: 1 addition & 0 deletions changelog.d/150.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Prompter popout windows now build same-origin `mode=prompter` URLs with the platform URL parser and isolate the opened window from `window.opener`.
1 change: 1 addition & 0 deletions changelog.d/151.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Presenter BroadcastChannel traffic is now scoped to an ephemeral workbench session, and prompter popout URLs carry that scope so independent same-origin presentations do not drive each other.
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
Loading
Loading