Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
127b21f
Add CI/CD workflow to deploy conductor modules to modules-conductor repo
martin-henz Apr 13, 2026
a1d84ff
Merge branch 'master' into conductor-migration
martin-henz Apr 16, 2026
0a4281d
Merge branch 'master' into conductor-migration
martin-henz Apr 24, 2026
6edaf88
Merge branch 'master' into conductor-migration
martin-henz May 5, 2026
699bb49
Merge branch 'master' into conductor-migration
martin-henz May 26, 2026
7c75a40
Initial Conductor Migration (Repeat Module Migration, Testing, Docume…
AaravMalani Jun 16, 2026
ec9d87d
Merge branch 'master' into conductor-migration
martin-henz Jul 15, 2026
bfce5cc
Merge branch 'master' into conductor-migration
martin-henz Jul 20, 2026
053d02c
Apply suggestions from code review
martin-henz Jul 20, 2026
ce1507a
Merge branch 'master' into conductor-migration
martin-henz Jul 20, 2026
9fcd431
Migrate binary_tree module to Conductor (#790)
Akshay-2007-1 Jul 20, 2026
605e383
Migrate Rune Module (#765)
AaravMalani Jul 20, 2026
81856f0
buildtools: register @publicType/@publicReturnType as known Typedoc b…
martin-henz Jul 20, 2026
a98c102
docs: fix 5-animations.md twoslash samples broken by rune's Conductor…
martin-henz Jul 20, 2026
7bdaa4e
Revert @functionDeclaration/@variableDeclaration decorator reimplemen…
martin-henz Jul 20, 2026
a508fd9
Migrate scrabble module to Conductor (#792)
Shrey5132 Jul 21, 2026
a93e4d5
Merge branch 'master' into conductor-migration
martin-henz Jul 21, 2026
6403d86
fix: pin @sourceacademy/conductor to a published version everywhere (…
Akshay-2007-1 Jul 21, 2026
d36647c
fix: scope the vite pin from #795 to vitest only (#811)
Akshay-2007-1 Jul 21, 2026
a1acc55
fix: guard against undefined children in typedoc-plugin's buildJson (…
Akshay-2007-1 Jul 21, 2026
2e29132
Migrate midi module to Conductor (#791)
Akshay-2007-1 Jul 21, 2026
1d46ef4
fix: regenerate yarn.lock, drifted out of sync after midi (#791) merge
martin-henz Jul 21, 2026
a1c5ecb
Merge remote-tracking branch 'origin/master' into conductor-migration
martin-henz Jul 23, 2026
961f641
Migrate Sound Module to Conductor (#796)
Akshay-2007-1 Jul 24, 2026
9a02a7e
sound: fix lint errors on conductor-migration (#818)
Akshay-2007-1 Jul 24, 2026
862effc
docs: fix CurveDrawn not being an exported type from bundle-curve/typ…
Akshay-2007-1 Jul 24, 2026
fe26745
Fix predicate methods declaring 0 arity instead of DataType.ANY (#821)
martin-henz Jul 25, 2026
aa37ecb
Bump @sourceacademy/conductor catalog pin to 0.8.0 (#822)
martin-henz Jul 25, 2026
67b1755
Migrate Matrix (Tone Matrix) bundle to Conductor (#802)
Akshay-2007-1 Jul 26, 2026
74a8de1
sound: constructor ordering, tab lookup by name, and a sync-probe cra…
Akshay-2007-1 Jul 26, 2026
d2b6f83
Merge branch 'master' into conductor-migration
martin-henz Jul 26, 2026
9313694
Migrate CSG Module to Conductor (#826)
martin-henz Jul 27, 2026
dfe8725
Merge branch 'master' into conductor-migration
martin-henz Jul 27, 2026
a6605b1
Migrate the `curve` and `plotly` modules (#840)
AaravMalani Jul 27, 2026
f106ec3
Fix conductor-migration CI regressions from PR #840 (#852)
martin-henz Jul 27, 2026
be625dc
Add missing @sourceacademy/modules-testplugin devDependency to curve …
martin-henz Jul 27, 2026
ab52c05
Add missing @sourceacademy/conductor devDependency to modules-buildto…
martin-henz Jul 27, 2026
1785e2d
Prebundle all @sourceacademy/conductor subpaths in shared tab test co…
martin-henz Jul 27, 2026
6c07c13
Fix unresolvable JSDoc @link references in curve bundle's module doc …
martin-henz Jul 27, 2026
9a64fba
Fix stale Curve type in docs conventions samples (#857)
martin-henz Jul 27, 2026
6ad0dce
fix: accept DataType.ARRAY as equally valid to PAIR for tree nodes (#…
Akshay-2007-1 Jul 28, 2026
9dda178
Fix TestDataHandler's DataType.ANY and PAIR/ARRAY handling (#859)
martin-henz Jul 28, 2026
e797221
Bump @sourceacademy/conductor to ^0.8.2 (#858)
martin-henz Jul 28, 2026
f1c2487
Merge remote-tracking branch 'origin/master' into conductor-migration
martin-henz Jul 28, 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
23 changes: 23 additions & 0 deletions .github/actions/src/info/__tests__/sorter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,27 @@ describe(topoSortPackages, () => {

expect(func).toThrowError('Dependency graph has a cycle!');
});

test('A dependency sharing the @sourceacademy scope but not a local workspace package (e.g. the real published @sourceacademy/conductor npm package) is ignored, not treated as a graph node', () => {
const result = topoSortPackages({
'@sourceacademy/0': {
type: null,
hasChanges: false,
directory: '/',
package: {
name: '@sourceacademy/0',
devDependencies: {},
dependencies: {
// Not a key of this packages record at all - a real npm dependency, not a workspace
// package - so it must not end up in the topological order (there's no
// RawPackageRecord for it, and processRawPackages would crash reading .hasChanges off
// undefined otherwise).
'@sourceacademy/conductor': 'npm:^0.7.1'
}
}
}
});

expect(result).toEqual(['@sourceacademy/0']);
});
});
6 changes: 5 additions & 1 deletion .github/actions/src/info/sorter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ export function topoSortPackages(packages: Record<string, RawPackageRecord>) {
}

for (const name of keys) {
if (name.startsWith('@sourceacademy')) {
// A dependency starting with @sourceacademy isn't necessarily a local workspace package -
// e.g. @sourceacademy/conductor is a real published npm package that just happens to share
// the org scope. Only graph it if it's an actual node in this monorepo; otherwise it has no
// RawPackageRecord and processRawPackages crashes reading .hasChanges off undefined.
if (name.startsWith('@sourceacademy') && name in packages) {
if (!(name in indegrees)) {
indegrees[name] = 1;
} else {
Expand Down
49 changes: 49 additions & 0 deletions .github/workflows/conductor-deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Deploy to modules-conductor

permissions:
contents: read

on:
push:
branches:
- conductor-migration

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
uses: actions/checkout@v6
with:
submodules: recursive

- name: Enable Corepack
run: corepack enable

- name: Use Node.js 💻
uses: actions/setup-node@v6
with:
node-version-file: .node-version
cache: yarn

- name: Install Dependencies 📦
run: yarn install --immutable

- name: Build Modules 🔧
run: yarn workspaces foreach -ptW --from "./src/{bundles,tabs}/*" run build

- name: Build Manifest
run: yarn buildtools manifest

- name: Build All Docs
run: yarn build:docs

- name: include java json
run: cp -r src/java build
Comment on lines +41 to +42

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: The cp -r src/java build command will cause the workflow to fail if the src/java directory does not exist, as there is no error handling.
Severity: HIGH

Suggested Fix

Wrap the cp command in a conditional check to ensure it only runs if the src/java directory exists. For example: if [ -d src/java ]; then cp -r src/java build; fi. This prevents the step from failing if the directory is absent.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: .github/workflows/conductor-deploy.yml#L38-L39

Potential issue: The `conductor-deploy.yml` workflow includes the command `cp -r
src/java build`. This command will fail with a non-zero exit code if the `src/java`
directory does not exist on the branch where the workflow is triggered. Since GitHub
Actions treats non-zero exit codes as step failures by default and there is no
conditional check or `continue-on-error` flag, the entire deployment workflow will halt
and fail. The `src/java` directory does not currently exist in the repository's main
branch, making this failure a realistic scenario.

Did we get this right? 👍 / 👎 to inform future reviews.


- name: Deploy 🚀
uses: peaceiris/actions-gh-pages@v4
with:
deploy_key: ${{ secrets.CONDUCTOR_DEPLOY_KEY }}
external_repository: source-academy/modules-conductor
publish_dir: ./build
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
2 changes: 2 additions & 0 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ npmMinimalAgeGate: 4320 # 3 days
# exempt our own first-party packages
npmPreapprovedPackages:
- js-slang
- '@sourceacademy/conductor'

# Supported from Yarn 4.10.0 onwards
# https://yarnpkg.com/features/catalogs
# Define here to avoid duplications
catalog:
'@sourceacademy/conductor': ^0.8.2
js-slang: ^1.0.94
react: ^19.0.0
react-dom: ^19.0.0
Expand Down
3 changes: 1 addition & 2 deletions docs/src/modules/2-bundle/4-conventions/3-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,13 @@ Specific to error handling, thrown errors should contain a reference to the call
```ts twoslash
import { GeneralRuntimeError } from '@sourceacademy/modules-lib/errors';
import type { Wave, Sound } from '@sourceacademy/bundle-sound/types';
import { pair } from 'js-slang/dist/stdlib/list';
// ---cut---
export function make_sound(wave: Wave, duration: number): Sound {
if (duration < 0) {
throw new GeneralRuntimeError(`${make_sound.name}: Sound duration must be greater than or equal to 0`);
}

return pair((t: number) => (t >= duration ? 0 : wave(t)), duration);
return { leftWave: wave, rightWave: wave, duration };
}
```

Expand Down
6 changes: 4 additions & 2 deletions docs/src/modules/2-bundle/4-conventions/4-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -585,9 +585,10 @@ export function run_callback(f: () => void) {
You can also provide arguments:

```ts twoslash
import type { Curve } from '@sourceacademy/bundle-curve/curves_webgl';
import { callWithoutMetadata } from '@sourceacademy/modules-lib/utilities';

type Curve = (t: number) => number;

export function makeNewCurve(c: Curve, numPoints: number): Curve {
for (let i = 0; i < numPoints; i++) {
const point = callWithoutMetadata(c, i / numPoints);
Expand All @@ -606,9 +607,10 @@ functions that are provided from the _standard library_ or from other module fun
`callIfFuncAndRightArgs` is used when you want to provide extra metadata about where the function was defined:

```ts twoslash
import type { Curve } from '@sourceacademy/bundle-curve/curves_webgl';
import { callIfFuncAndRightArgs } from '@sourceacademy/modules-lib/utilities';

type Curve = (t: number) => number;

export function evaluateCurve(c: Curve, numPoints: number) {
for (let i = 0; i < numPoints; i++) {
// Line Number, Column Number, File name, NativeStorage (usually undefined), ...args
Expand Down
22 changes: 11 additions & 11 deletions docs/src/modules/3-tabs/1-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,25 +90,25 @@ export default defineTab({

Here is an example of a tab object:

```tsx twoslash [Sound/src/index.tsx]
```tsx twoslash [Curve/src/index.tsx]
// @jsx: react-jsx
import type { AudioPlayed } from '@sourceacademy/bundle-sound/types';
declare function SoundTab(props: { elements: AudioPlayed[] }): React.ReactElement;
import type { CurveModuleState as _CurveModuleState } from '@sourceacademy/bundle-curve/types';
declare function CurveRenderer(props: { elements: _CurveModuleState['drawnCurves'] }): React.ReactElement;
// ---cut---
import type { SoundModuleState } from '@sourceacademy/bundle-sound/types';
import type { CurveModuleState } from '@sourceacademy/bundle-curve/types';
import { defineTab, getModuleState } from '@sourceacademy/modules-lib/tabs/utils';

export default defineTab({
toSpawn(context) {
const state = getModuleState<SoundModuleState>(context, 'sound');
return !!state && state.audioPlayed.length > 0;
const state = getModuleState<CurveModuleState>(context, 'curve');
return !!state && state.drawnCurves.length > 0;
},
body(context) {
const { audioPlayed } = getModuleState<SoundModuleState>(context, 'sound')!;
return <SoundTab elements={audioPlayed} />;
const { drawnCurves } = getModuleState<CurveModuleState>(context, 'curve')!;
return <CurveRenderer elements={drawnCurves} />;
},
label: 'Sound Tab',
iconName: 'music'
label: 'Curve Tab',
iconName: 'graph'
});
```

Expand Down Expand Up @@ -152,6 +152,6 @@ Note that `IconName`s are defined in `@blueprintjs/core`, but it is not necessar

A tab is only evaluated once per Source evaluation. Only one instance of a given tab can be spawned at the same time. Your tab and bundle should be designed with this in mind.

For example, the `sound` bundle stores in its state an array of `AudioPlayed` objects, which allows the single tab to handle multiple calls to `play_in_tab`, which is the function in the bundle that causes the tab to spawn.
For example, the `curve` bundle stores in its state an array of drawn curves, which allows the single tab to handle multiple draw calls across a program's evaluation, accumulating them all into one tab instance instead of spawning a new one per call.

Other bundles like `repl` have a single instance. The bundle never spawns a second instance of its programmable REPL, so the single REPL tab that spawns handles everything.
23 changes: 12 additions & 11 deletions docs/src/modules/3-tabs/3-editing.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,9 @@ test('Matches snapshot', () => {
> property accesses or object destructuring:
>
> ```ts
> const { context: { moduleContexts: { sound: { state: { audioPlayed } } } } } = context;
> const { context: { moduleContexts: { curve: { state: { drawnCurves } } } } } = context;
> // or
> const audioPlayed = context.context.moduleContexts.sound.state.audioPlayed;
> const drawnCurves = context.context.moduleContexts.curve.state.drawnCurves;
> ```
>
> The risk here is that each one of these property accesses could return `null` or `undefined`, resulting
Expand All @@ -208,8 +208,8 @@ test('Matches snapshot', () => {
> ```ts
> const toSpawn = (context: DebuggerContext): boolean => {
> // Can't use destructuring with null coalescing
> const audioPlayed = context.context.moduleContexts?.sound?.state?.audioPlayed;
> return audioPlayed && audioPlayed.length > 0;
> const drawnCurves = context.context.moduleContexts?.curve?.state?.drawnCurves;
> return drawnCurves && drawnCurves.length > 0;
> };
> ```
>
Expand All @@ -218,14 +218,14 @@ test('Matches snapshot', () => {
>
> ```ts twoslash
> import type { DebuggerContext } from '@sourceacademy/modules-lib/types';
> import type { SoundModuleState } from '@sourceacademy/bundle-sound/types';
> import type { CurveModuleState } from '@sourceacademy/bundle-curve/types';
> // ---cut---
> import { getModuleState } from '@sourceacademy/modules-lib/tabs/utils';
>
> const toSpawn = (context: DebuggerContext): boolean => {
> // Can't use destructuring with null coalescing
> const state = getModuleState<SoundModuleState>(context, 'sound');
> return !!state && state.audioPlayed.length > 0;
> const state = getModuleState<CurveModuleState>(context, 'curve');
> return !!state && state.drawnCurves.length > 0;
> };
> ```
>
Expand All @@ -234,14 +234,15 @@ test('Matches snapshot', () => {
> ```tsx twoslash
> // @jsx: react-jsx
> import type { DebuggerContext } from '@sourceacademy/modules-lib/types';
> import type { AudioPlayed, SoundModuleState } from '@sourceacademy/bundle-sound/types';
> declare function SoundTab(props: { elements: AudioPlayed[] }): React.ReactElement;
> import type { CurveModuleState as _CurveModuleState } from '@sourceacademy/bundle-curve/types';
> declare function CurveRenderer(props: { elements: _CurveModuleState['drawnCurves'] }): React.ReactElement;
> // ---cut---
> import type { CurveModuleState } from '@sourceacademy/bundle-curve/types';
> import { getModuleState } from '@sourceacademy/modules-lib/tabs/utils';
>
> const body = (context: DebuggerContext): React.ReactElement => {
> const { audioPlayed } = getModuleState<SoundModuleState>(context, 'sound')!;
> return <SoundTab elements={audioPlayed} />;
> const { drawnCurves } = getModuleState<CurveModuleState>(context, 'curve')!;
> return <CurveRenderer elements={drawnCurves} />;
> };
> ```
>
Expand Down
6 changes: 3 additions & 3 deletions docs/src/modules/3-tabs/5-animations/5-animations.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,9 @@ export default function HollusionCanvas({ rune }: Props) {
const renderFuncRef = useRef<(time: number) => void>(undefined);

const { setCanvas } = useAnimation({
callback({ timestamp, canvas }) {
async callback({ timestamp, canvas }) {
if (renderFuncRef.current === undefined) {
renderFuncRef.current = rune.draw(canvas);
renderFuncRef.current = await rune.draw(canvas);
}
renderFuncRef.current(timestamp);
},
Expand Down Expand Up @@ -244,7 +244,7 @@ type RuneAnimation = (t: number) => Rune;
But Source doesn't have the ability to enforce this at runtime, so a cadet might accidentally return something that isn't a rune:

```ts twoslash
// @noErrors: 2322
// @errors: 2614 7006
import { animate_rune, heart } from '@sourceacademy/bundle-rune';

animate_rune(1, 60, t => t < 60 ? heart : 0);
Expand Down
26 changes: 13 additions & 13 deletions docs/src/modules/5-advanced/context.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,21 @@ Some times, a bundle needs to be able to maintain some state information, or sen

Every time `js-slang` evaluates Source code, it creates an evaluation context. Bundles can access this context by using this import:

```ts twoslash [sound/functions.ts]
```ts twoslash [curve/functions.ts]
// @paths: { "js-slang/context": ["./context.d.ts"] }
// @filename: context.d.ts
import type { Context } from 'js-slang';
declare const ctx: Context;
export default ctx;

// @filename: curve.ts
import type { AudioPlayed } from '@sourceacademy/bundle-sound/types';
import type { CurveModuleState } from '@sourceacademy/bundle-curve/types';
// ---cut---
import context from 'js-slang/context';

const audioPlayed: AudioPlayed[] = [];
context.moduleContexts.sound.state = {
audioPlayed
const drawnCurves: CurveModuleState['drawnCurves'] = [];
context.moduleContexts.curve.state = {
drawnCurves
};
```

Expand All @@ -31,22 +31,22 @@ The `state` object can be of any type - it is up to the developer to decide what

This `state` object can then be accessed by the module's tab, for example:

```tsx twoslash [Sound/index.tsx]
```tsx twoslash [Curve/index.tsx]
// @jsx: react-jsx
import type { DebuggerContext } from '@sourceacademy/modules-lib/types';
declare function SoundTab(props: { context: DebuggerContext }): React.ReactElement;
declare function CurveRenderer(props: { context: DebuggerContext }): React.ReactElement;
// ---cut---
import { defineTab, getModuleState } from '@sourceacademy/modules-lib/tabs/utils';
import type { SoundModuleState } from '@sourceacademy/bundle-sound/types';
import type { CurveModuleState } from '@sourceacademy/bundle-curve/types';

export default defineTab({
toSpawn: context => {
const state = getModuleState<SoundModuleState>(context, 'sound');
return !!state && state.audioPlayed.length > 0;
const state = getModuleState<CurveModuleState>(context, 'curve');
return !!state && state.drawnCurves.length > 0;
},
body: (context) => <SoundTab context={context} />,
label: 'Sounds',
iconName: 'music'
body: (context) => <CurveRenderer context={context} />,
label: 'Curves',
iconName: 'graph'
});
```

Expand Down
10 changes: 8 additions & 2 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,9 @@ export default defineConfig(
'categoryDescription',
'defaultValue',
'hidden',
'title'
'title',
'publicType',
'publicReturnType'
],
inlineTags: ['link', 'see'],
}],
Expand Down Expand Up @@ -304,6 +306,7 @@ export default defineConfig(
}
],
'prefer-const': ['warn', { destructuring: 'all' }],
'require-yield': 'off',

'@sourceacademy/default-import-name': ['warn', { path: 'pathlib' }],
'@sourceacademy/no-barrel-imports': ['error', ['lodash']],
Expand Down Expand Up @@ -443,7 +446,10 @@ export default defineConfig(
ignores: ['src/bundles/**/__tests__/*.ts*'],
rules: {
// Rule doesn't work properly on CI
'@sourceacademy/throw-runtime-error': process.env.CI ? 'off' : 'error'
'@sourceacademy/throw-runtime-error': process.env.CI ? 'off' : ['error', {
// Conductor's own protocol-level errors, unrelated to js-slang's RuntimeSourceError
ignoredNames: ['EvaluatorTypeError', 'EvaluatorRuntimeError', 'EvaluatorParameterTypeError']
}]
}
},
{
Expand Down
1 change: 1 addition & 0 deletions lib/buildtools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"version": "1.1.0",
"devDependencies": {
"@commander-js/extra-typings": "^15.0.0",
"@sourceacademy/conductor": "catalog:",
"@types/estree": "^1.0.0",
"@types/http-server": "^0.12.4",
"@types/node": "^24.0.0",
Expand Down
Loading
Loading