Skip to content

Commit 512d355

Browse files
authored
Merge pull request #3 from kcleland0818/prep/cs
Prep/cs
2 parents 9f96ddb + 26a3e68 commit 512d355

11 files changed

Lines changed: 556 additions & 19 deletions

.github/workflows/build-release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ jobs:
3636
3737
- name: Create release tarball
3838
run: |
39-
tar -czf release.tar.gz dist/ package.json server.js node_modules/
39+
tar -czf release.tar.gz dist/ package.json config.json server.js node_modules/
4040
4141
- name: Upload build artifact (for workflow logs)
4242
uses: actions/upload-artifact@v4

AGENTS.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ This template provides:
4040
```
4141
Server runs on `http://localhost:3000`
4242

43+
## CodeSignal Runtime Contract
44+
45+
- Root `config.json` is loaded through `GET /config`; task setup scripts may replace it before server startup.
46+
- The Geometry Explorer client reads `initialState.mode`, `initialState.shape`, and `initialState.values` to initialize the app.
47+
- The client posts current state to `POST /snapshot`; evaluators can read it with `GET /snapshot`.
48+
- Release tarballs must include `dist/`, `package.json`, `config.json`, `server.js`, and production `node_modules/`.
49+
4350
## Key Conventions
4451

4552
### File Naming
@@ -123,4 +130,4 @@ When working on applications built with this template:
123130
3. **Use Design System components** directly - see BESPOKE-TEMPLATE.md for
124131
component classes and usage
125132
4. **Maintain consistency** with the template's structure and patterns
126-
5. **Keep guidelines up to date** by editing this AGENTS.md file as the codebase evolves
133+
5. **Keep guidelines up to date** by editing this AGENTS.md file as the codebase evolves

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,34 @@ curl -X POST http://localhost:3000/message \
172172
-d '{"message": "Hello from the server!"}'
173173
```
174174

175+
### Runtime Config and Snapshot API
176+
177+
CodeSignal task setup can replace the root `config.json` before starting the server. The app reads this through `GET /config` on load and initializes the selected shape and dimensions from `initialState`.
178+
179+
Example `config.json`:
180+
181+
```json
182+
{
183+
"version": 1,
184+
"initialState": {
185+
"mode": "3d",
186+
"shape": "cylinder",
187+
"values": {
188+
"radius": 3,
189+
"height": 8
190+
}
191+
},
192+
"ui": {
193+
"lockedMode": false,
194+
"lockedShape": false,
195+
"showFormulaHints": true
196+
},
197+
"evaluation": {}
198+
}
199+
```
200+
201+
The browser posts the latest simulation state to `POST /snapshot` whenever the learner changes dimensions, mode, or shape. Evaluators can call `GET /snapshot` to read the latest state, including computed metrics.
202+
175203
## CI/CD and Automated Releases
176204

177205
This template includes a GitHub Actions workflow (`.github/workflows/build-release.yml`) that automatically builds and releases your application when you push to the `main` branch.

client/geometry-explorer.js

Lines changed: 141 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import NumericSlider from './design-system/components/numeric-slider/numeric-sli
66
const U = 'u';
77
const U2 = 'u²';
88
const U3 = 'u³';
9+
const DEFAULT_MODE = '2d';
10+
const DEFAULT_SHAPE_2D = 'rectangle';
11+
const DEFAULT_SHAPE_3D = 'prism';
12+
const SNAPSHOT_DEBOUNCE_MS = 250;
913

1014
function formatNumber(n) {
1115
if (!Number.isFinite(n)) return '—';
@@ -176,6 +180,88 @@ const SHAPES_3D = {
176180
},
177181
};
178182

183+
function getCatalog(mode) {
184+
return mode === '3d' ? SHAPES_3D : SHAPES_2D;
185+
}
186+
187+
function getCurrentShapeKey(state) {
188+
return state.mode === '3d' ? state.shape3d : state.shape2d;
189+
}
190+
191+
function getShapeDefForState(state) {
192+
return getCatalog(state.mode)[getCurrentShapeKey(state)];
193+
}
194+
195+
function snapToStep(value, min, step) {
196+
if (!Number.isFinite(step) || step <= 0) return value;
197+
const snapped = Math.round((value - min) / step) * step + min;
198+
return Number(snapped.toFixed(6));
199+
}
200+
201+
function normalizeValues(def, inputValues = {}) {
202+
const values = inputValues && typeof inputValues === 'object' ? inputValues : {};
203+
const out = {};
204+
def.keys.forEach((key) => {
205+
const raw = values[key];
206+
const numeric = Number(raw);
207+
const fallback = def.defaults[key];
208+
const value = Number.isFinite(numeric) ? numeric : fallback;
209+
const clamped = Math.min(def.ranges.max, Math.max(def.ranges.min, value));
210+
out[key] = snapToStep(clamped, def.ranges.min, def.ranges.step);
211+
});
212+
return out;
213+
}
214+
215+
function normalizeInitialState(config) {
216+
const initial = config?.initialState || config?.state || {};
217+
const requestedMode = initial.mode === '3d' ? '3d' : DEFAULT_MODE;
218+
const catalog = getCatalog(requestedMode);
219+
const fallbackShape = requestedMode === '3d' ? DEFAULT_SHAPE_3D : DEFAULT_SHAPE_2D;
220+
const requestedShape =
221+
typeof initial.shape === 'string'
222+
? initial.shape
223+
: requestedMode === '3d'
224+
? initial.shape3d
225+
: initial.shape2d;
226+
const shape = Object.prototype.hasOwnProperty.call(catalog, requestedShape)
227+
? requestedShape
228+
: fallbackShape;
229+
const def = catalog[shape];
230+
const values = normalizeValues(def, initial.values);
231+
232+
return {
233+
mode: requestedMode,
234+
shape2d: requestedMode === '2d' ? shape : DEFAULT_SHAPE_2D,
235+
shape3d: requestedMode === '3d' ? shape : DEFAULT_SHAPE_3D,
236+
values,
237+
};
238+
}
239+
240+
async function loadRuntimeConfig() {
241+
try {
242+
const response = await fetch('/config', { cache: 'no-store' });
243+
if (!response.ok) {
244+
console.warn('Geometry Explorer config request failed:', response.status);
245+
return {};
246+
}
247+
const config = await response.json();
248+
return config && typeof config === 'object' ? config : {};
249+
} catch (error) {
250+
console.warn('Geometry Explorer config could not be loaded; using defaults.', error);
251+
return {};
252+
}
253+
}
254+
255+
function buildSnapshot(state, metrics) {
256+
return {
257+
version: 1,
258+
mode: state.mode,
259+
shape: getCurrentShapeKey(state),
260+
values: { ...state.values },
261+
metrics: { ...metrics },
262+
};
263+
}
264+
179265
function draw2D(ctx, canvas, shapeKey, values) {
180266
const { w, h } = getLogicalCanvasSize(canvas);
181267
ctx.clearRect(0, 0, w, h);
@@ -529,7 +615,7 @@ function draw3D(ctx, canvas, shapeKey, values) {
529615
}
530616
}
531617

532-
function initGeometryExplorer() {
618+
async function initGeometryExplorer() {
533619
const canvas = document.getElementById('geometry-canvas');
534620
const modeSelect = document.getElementById('geometry-mode');
535621
const shapeSelect = document.getElementById('geometry-shape');
@@ -538,19 +624,14 @@ function initGeometryExplorer() {
538624
const formulaEl = document.getElementById('geometry-formula-note');
539625
if (!canvas || !modeSelect || !shapeSelect || !slidersRoot || !metricsList) return;
540626

627+
const runtimeConfig = await loadRuntimeConfig();
541628
const ctx = canvas.getContext('2d');
542629
let sliders = [];
543-
let state = {
544-
mode: '2d',
545-
shape2d: 'rectangle',
546-
shape3d: 'prism',
547-
values: { ...SHAPES_2D.rectangle.defaults },
548-
};
630+
let snapshotTimer = null;
631+
let state = normalizeInitialState(runtimeConfig);
549632

550633
function getShapeDef() {
551-
return state.mode === '2d'
552-
? SHAPES_2D[state.shape2d]
553-
: SHAPES_3D[state.shape3d];
634+
return getShapeDefForState(state);
554635
}
555636

556637
function readValuesFromSliders() {
@@ -562,7 +643,46 @@ function initGeometryExplorer() {
562643
return out;
563644
}
564645

565-
function syncMetrics() {
646+
async function postSnapshot(snapshot) {
647+
try {
648+
await fetch('/snapshot', {
649+
method: 'POST',
650+
headers: { 'Content-Type': 'application/json' },
651+
body: JSON.stringify(snapshot),
652+
});
653+
} catch (error) {
654+
console.warn('Geometry Explorer snapshot could not be saved.', error);
655+
}
656+
}
657+
658+
function publishSnapshot(metrics, immediate = false) {
659+
const snapshot = buildSnapshot(state, metrics);
660+
if (snapshotTimer) {
661+
clearTimeout(snapshotTimer);
662+
snapshotTimer = null;
663+
}
664+
665+
if (immediate) {
666+
postSnapshot(snapshot);
667+
return;
668+
}
669+
670+
snapshotTimer = setTimeout(() => {
671+
snapshotTimer = null;
672+
postSnapshot(snapshot);
673+
}, SNAPSHOT_DEBOUNCE_MS);
674+
}
675+
676+
function applyUiConfig() {
677+
const ui = runtimeConfig.ui || {};
678+
modeSelect.disabled = ui.lockedMode === true;
679+
shapeSelect.disabled = ui.lockedShape === true;
680+
if (formulaEl) {
681+
formulaEl.hidden = ui.showFormulaHints === false;
682+
}
683+
}
684+
685+
function syncMetrics({ publish = true, immediate = false } = {}) {
566686
const def = getShapeDef();
567687
state.values = readValuesFromSliders();
568688
const computed = def.compute(state.values);
@@ -614,6 +734,10 @@ function initGeometryExplorer() {
614734
} else {
615735
draw3D(ctx, canvas, state.shape3d, state.values);
616736
}
737+
738+
if (publish) {
739+
publishSnapshot(computed, immediate);
740+
}
617741
}
618742

619743
function rebuildSliders() {
@@ -657,8 +781,8 @@ function initGeometryExplorer() {
657781
}
658782

659783
function populateShapeOptions() {
660-
const catalog = state.mode === '2d' ? SHAPES_2D : SHAPES_3D;
661-
let currentKey = state.mode === '2d' ? state.shape2d : state.shape3d;
784+
const catalog = getCatalog(state.mode);
785+
let currentKey = getCurrentShapeKey(state);
662786
const keys = Object.keys(catalog);
663787
if (!keys.includes(currentKey)) {
664788
currentKey = keys[0];
@@ -701,16 +825,17 @@ function initGeometryExplorer() {
701825

702826
const frameEl = canvas.parentElement;
703827
const ro = new ResizeObserver(() => {
704-
syncMetrics();
828+
syncMetrics({ publish: false });
705829
});
706830
if (frameEl) {
707831
ro.observe(frameEl);
708832
}
709833

710-
populateShapeOptions();
834+
applyUiConfig();
711835
modeSelect.value = state.mode;
836+
populateShapeOptions();
712837
rebuildSliders();
713-
syncMetrics();
838+
syncMetrics({ immediate: true });
714839
}
715840

716841
export { initGeometryExplorer };

config.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"version": 1,
3+
"initialState": {
4+
"mode": "2d",
5+
"shape": "circle",
6+
"values": {
7+
"radius": 6
8+
}
9+
},
10+
"ui": {
11+
"lockedMode": false,
12+
"lockedShape": true,
13+
"showFormulaHints": false
14+
},
15+
"evaluation": {
16+
"target": {
17+
"metric": "perimeter",
18+
"value": 37.699
19+
},
20+
"tolerance": 0.01
21+
}
22+
}

0 commit comments

Comments
 (0)