Conversation
… configs Opt-in gainModel: 'fixed' renders wire antennas (QFH, crossed yagi, GPS patch) with explicit gain/HPBW instead of parabolic aperture math, capped off-axis at a front-to-back ratio. Wide-beam antennas accept satellites out to one HPBW using true great-circle separation (the planar +/-1 deg box breaks near zenith). Adds circular handedness selection with config-driven cross-pol loss, VHF/UHF band entries, and three DIY antenna configs. All changes are opt-in; parabolic antennas are bit-identical.
…ass-through isDirectSampling bypasses the LNB mixer (RF = IF) with an RTL-SDR-style 24-1766 MHz passband. An OMT configured for LHCP/RHCP becomes a pass-through so handedness discrimination is charged once, at the antenna feed, never double-counted. Both opt-in; legacy H/V and block-downconversion paths unchanged.
Opt-in isAfcEnabled slews the modem center toward the received carrier (200 Hz/tick, 10 Hz deadband) to track Doppler drift. Adds public handleModemFrequencyChange/handleModemConfigChange for UI layers (SDR Console). Defaults preserve fully manual tuning.
…ndwidth with PSD scaling getInputSignals() clamped every displayed signal's bandwidth to the RBW, so a 2 MHz GPS hump at 30 kHz RBW drew as a 30 kHz needle (and a 36 MHz video at 1 MHz RBW as a 1 MHz spike). RBW sets resolution and the displayed noise floor, not a signal's occupied width. Removes the clamp and renders signals wider than the RBW at per-RBW-bin power (PSD), so widths and heights are both honest against the per-RBW noise floor. Display-only; no physics or objective changes. Also exposes the spectrum data processor for campaign UIs.
…ion tab set Opt-in stationClass: 'backyard' replaces the professional tab set with SDR Console + Observations (Pass Schedule reskin; gating broadened to any OrbitalSatellite so MEO birds appear). The console drives the same headless cores as the professional tabs: spectrum trace with dB axis, waterfall with VFO passband overlay and band plan ribbon, digit-wise VFO readout, click/scroll tuning, bookmarks from scenario downlinks, mode/FEC/BW controls, AFC, polarization switch, rotctl-style rotator panel, RF gain with real ADC clipping/quantization consequences, and a decoder with RDS-style ID. S-meter reads passband energy when the demod has no carrier and the lock indicator self-diagnoses (BW TOO WIDE / BW TOO NARROW / MODE?). NOTE: audio demodulation is a future phase (see header comment). Professional stations render exactly as before.
Activates the ham-sdr campaign stub as 'Backyard Operator': Riley Brooks (KD2RLY) teaches satellite tracking from a Burlington VT backyard. Three receive-only stations (137 MHz QFH, 70cm crossed yagi on a TV rotator, GPS L1 patch) with direct-sampling SDR chains, and three authored-TLE birds (WXSAT-19 APT at AOS T+3, CUBEHOP-1 FM at T+18, NAVSTAR-77 GPS overhead from T+0) validated by deterministic pass tests. Sandbox objectives exercise every new mechanic: observations list, QFH first contact + decode, yagi program-track with RHCP handedness, Doppler chase/AFC, and GPS spread-spectrum detection. Checks in the TLE grid-search authoring tool (C2 retro action item) and fixes the duplicate 'ccs' campaign id on the geolocation stub.
body.campaign-ham-sdr reskins the app as 2000s hobbyist freeware: phosphor-green accent, Verdana/Tahoma system fonts, zero border radius, beveled outset/inset buttons. Scoped to the campaign body class; --mc-danger* untouched so faults stay red everywhere.
Phase plan, phase retrospective, and a cross-cutting post-mortem covering the four live-only defects (reception box, decode noise gate, RBW render clamp, mute lock failures) and ten lessons learned - including instrumenting over theorizing, recomputed-config traps, and self-diagnosing instruments.
…rence events
Optional emitter { latitude, longitude, altitudeKm } on InterferenceEventConfig
plus getEvent/getEventsWithEmitters query API. Absent by default, so existing
jamming scenarios (e.g. nats-scenario21) are unchanged; present only for
Campaign 5 geolocation gameplay.
Pure math core (no DOM/EventBus): TDOA/FDOA forward model over SGP4 satellite positions, noisy measurement synthesis, weighted grid-search + Gauss-Newton solver with a 95% error ellipse, and an lopResidual primitive for line-of- position tracing. Unit-tested for exact and noisy recovery, ellipse tightening, and clustered-capture latitude observability.
…ission Control tab - GeolocationConsoleCore: correlator tuning, duty-cycle-gated capture, and an auto-capture toggle that retries across the interferer's on/off cycle. - GeoMap: full-Earth basemap (earthmap4k.jpg) with scroll-zoom/drag-pan and viewport-traced TDOA/FDOA lines of position that stay solid at any zoom. - Geolocation tab: correlator + captured-measurements side by side over a full-width map; registered only when settings.geolocation is present. - SimulationSettings gains the opt-in geolocation block and the interference emitter field; console lifecycle wired into base-page/mission-control-page. Opt-in throughout, so Campaigns 1-4 are unaffected.
…itions New additive condition types geolocation-measurements-collected (minCount) and geolocation-fix-accuracy (maxErrorKm), evaluated against GeolocationConsoleCore. Existing evaluator cases untouched.
…eolocation sandbox - Peterson Annex (PA-22) C-band station and two inclined-GEO OrbitalSatellite birds (SENTRY-7 victim, SENTRY-9 adjacent collector), TLEs authored against the scenario epoch by scripts/author-tle-signal-hunter.mjs. The 3.0/4.5 deg inclinations make the emitter latitude observable from clustered captures. - Sandbox scenario: detect -> characterize duty cycle -> geolocate -> report, with a hidden West Texas emitter. - Activates the campaign (id signal-hunter, replacing the duplicate ccs stub id).
Scoped body.campaign-signal-hunter block: near-black surfaces with a coyote brown accent, distinct from Campaign 1 (red) and Campaign 2 (blue). Semantic --mc-danger* left untouched so faults stay red in every campaign.
… spec Registry test guards unique campaign ids (the old ccs collision) and the Signal Hunter activation. E2E spec drives the console end to end: theme applied, tab present, duty-cycle-gated captures, and a computed fix.
…d EA console Activates the ccs campaign stub with offensive SATCOM-denial mechanics: ElectronicAttackManager (player-driven jam chain with J/S assessment), scheduled transmit-string hardware faults, own-force deconfliction interlock (protected-frequency fratricide fail), jamming objective conditions, EA Assessment tab, and the black-ops OD-green theme.
Eight opt-in mechanics for the nats-eu campaign, each gated by its own SimulationSettings block so Campaigns 1/3/4/5 are unaffected: - M1 LinkBudgetManager (Friis C/N worksheet + margin commit) - M2/M5 CommandingManager (uplink Doppler, TT&C command queue, key ops) - M3 ContactScheduleManager (multi-station pass allocation + conflicts) - M4 SpaceEventManager (scheduled maneuver -> stale TLE -> reacquire) - M6 SecurityConsoleCore (SOC-lite audit log + access control) - M7 TransecManager (fixed/hopping waveform + keyed hop-sync) - M8 GnssThreatManager (spoof window + timing drift + reference-mode defense) Adds 15 ConditionTypes + evaluators, OrbitalSatellite.reloadTle(), SimulationManager.hasInstance(), manager lifecycle wiring, the nats-eu sandbox that turns them all on, and a proof test driving each manager from the real sandbox settings.
Four gated Mission Control tabs make the nats-eu mechanics human-driveable: - link-budget (M1): Friis worksheet -> Compute -> Commit with acceptance badges - commanding (M2/M5): Doppler switch, command window, key lifecycle, guarded zeroize, canned + manual TT&C sends with a reject-reason log - contact-schedule (M3): per-contact station selects + live conflict/plan status - security-console (M6+M7): audit log flag/review, access-control accounts, TRANSEC panel (mode, hop-set key, sync badge) Tab registration is gated on the same opt-in settings blocks, so the legacy nats campaign registers none of them. Adds DOM-level click-through tests plus tabbed-canvas gating tests (absent by default, present with blocks, transec-only).
Campaign 4 counter-comms plan + retro, the full 24-scenario nats-eu campaign design plan, phase-2 mechanics and phase-3 console-tabs retros, and docs-local analysis notes (nats campaign analysis, campaign-5 lessons).
Snapshots the valid NIST NICE codes into a checked-in scripts/nice-catalog.json (542 codes) so the CI test is self-contained rather than depending on the gitignored .claude/ skill reference files, while gen-nice-catalog.mjs keeps it regenerable from those sources. nice-coverage.mjs prints the product-wide coverage table and fails on any code not in the catalog; nice-coverage.test.ts enforces that in CI. It immediately caught an off-catalog code in the signal-hunter sandbox (K0108 -> K1032). Adds npm run nice-coverage / nice-catalog.
author-passes.mjs grid-searches TLEs that produce requested pass windows over a given station and self-verifies the result with a pass table, so authoring "give me a pass at T+X over station Y" stops being hand-tuned trial and error. Reproduces the checked-in MERIDIAN-SAR-1 pass as its correctness demo. Pass authoring at scale is the structural cost of the 24-scenario Campaign 2 arc; this is the tool that attacks it.
KU_BAND_4M_LEO_TRACKER was configured at 5 deg/s in azimuth, which is unrealistically slow for a purpose-built Ku LEO tracker (real pedestals do 20-30 deg/s). At 5 deg/s the pedestal could not hold the narrow ~0.45 deg beam through even a moderate pass, so C/N collapsed mid-pass for reasons no operator could diagnose. Found by playtesting the live app, not by unit tests: the displayed downlink C/N only reached ~6.4 dB where analysis predicted ~14 dB. The link budget was never the problem - the pointing loop was. A near-zenith pass still hits an azimuth keyhole (rate -> infinity at the zenith); that is real physics and is reserved as a dedicated later lesson rather than engineered away.
LEO pass scenarios put objectives many simulated minutes apart, so an E2E spec that waits wall-clock time is infeasible. advanceClock() jumps the scenario clock in one step and is exposed on window for tests, mirroring the existing window.debugSignalPath hook in RFFrontEndCore. Orbital physics read absolute sim time via getSimulatedNowMs() and follow on the next tick; mission/objective timers tick on real update deltas and are deliberately unaffected by the jump.
The 1290 MHz transponded return that appears when the HPA comes up is now a decision graded on live evidence facts (no interference event, no injected fault, crypto intact), with the HPA state and RX key status as the evidence the operator must read first. secure-for-decode follows it. The decision box no longer shares the quiz box class, which made the shared e2e quiz locator ambiguous; it also clears its option buttons on close.
Night receive shift with three things that do not belong: a password spray in the overnight log, a terrestrial Ku carrier inside the SAR-1 video band that does not Doppler and outlives the pass, and a live configuration export by a decommissioned service account. Priya Sharma debuts. call-the-carrier is the first Gray Zone decision, graded on live evidence facts. Adds the Phase 3 validation test; registers the scenario in the manager, campaign, duration and sweep-quiz counts.
The S17 emitter returns on both passes, cycling 60 s on / 30 s off and sitting in the band of whichever bird is being received: the configuration export in use. Characterise the cadence, call it, notch it and keep the pass, log the event, notch it again when it follows the plan, file the regulator package. The interference-active evidence fact now reads the event envelope rather than the instantaneous radiating state, so a duty-cycled jammer is one incident and a decision about it cannot flip in its off phase.
…plink jammer Gray Zone arc 3/8. The SAR-2 commanding pass: HK-DUMP ACKs, then a transponder-path jammer on the 14035 MHz uplink from 13:22:30 and PLD-STATUS NAKs "Uplink denied - carrier jammed" with the key Valid, Doppler engaged and the window open. The operator calls the denial (decision graded on the new uplink-jammed fact with the key status and console state as evidence), keys the hop set both ends, takes the waveform to hopping, gets SYNC LOCKED, and the resend ACKs. SAR-1 receive-only afterwards, untouched. 17 objectives, 190 pts, 6 clips. Engine: CommandingManager.isUplinkJammed - a transponder-path interference event on the target bird overlapping settings.commanding.uplinkFrequencyHz (bandwidth/2 + UPLINK_JAM_GUARD_HZ) while in its envelope and TRANSEC not hop-synced rejects commands 'uplink-jammed'. New evidence fact 'uplink-jammed'; SimulationSettings.commanding.uplinkFrequencyHz; console reject copy; e2e sendCommandAndExpectNak helper; unit tests for the coupling and the fact; S19 registered in scenario-manager, campaign data, board sweep and phase-3 validation.
Gray Zone arc 4/8. Pre-dawn receive shift; a GNSS spoofer local to GW-01 from 05:08 to 05:36 walks the GNSS-vs-reference timing offset at 2 us/s while the constellation panel stays LOCKED with a full satellite count. Shetland reads +0.0. The operator reads the walk, cross-checks SH-02, calls the spoof (decision graded on timing-drifting AND gnss-constellation-healthy), takes the GNSS switch down, flies SAR-1 in holdover, flags Rotterdam's timestamp-skew audit entry and reports in progress, probes the sky after LOS (a second, clock-dependent decision: still walking before 05:36), and comes back to GNSS only when the offset holds still with GNSS re-selected. SAR-2 on true time. 17 objectives, 190 pts, 7 clips. Engine: GPS Timing tab gains a 'GNSS vs REF delta-T' readout (#gpsdo-time-offset) fed by GnssThreatManager, and its GNSS switch now sets the manager's reference mode (the coupling the SDR console already had). GnssThreatManager: affectsStation/timeOffsetUsFor (groundStationIds scoping so a second station is the cross-check) and secondsSinceOffsetChange. New conditions gpsdo-time-offset-exceeds (minOffsetUs) and gpsdo-time-offset-stable (holdSeconds). S20 registered in scenario-manager, campaign data (725-760 min), board sweep (13) and phase-3 validation; M8 mechanics test covers the scoping and the hold counter.
A scenario's Working Document is filed into a campaign-scoped store
(localStorage, keyed by campaign id) when it completes, with any audit-event
ids a destroyEvidence consequence removed during the run. A read-only
Campaign Record panel (sidebar, next to Working Doc) lists every earlier
scenario's record - the player's own where it was completed in this browser,
otherwise the scenario's canonical documentLines marked as filed - and the
campaign-document-reviewed condition latches on opening it.
The evidence chain crosses scenarios: an audit entry can declare
requiresCampaignEvidence { scenarioId, eventId }; SecurityConsoleCore drops
it at load when the record says a decision destroyed that evidence earlier,
and the new evidence-chain-intact fact reads false so a later decision can
grade attributable against unattributable on what the player kept. The
security console now copies its event list so a replay starts pristine.
S21 "Knocking on the Door": the SAR-2 commanding pass; replayed HK-DUMP frames rejected at the bird with a stale counter, every key badge Valid. call-the-knock graded on crypto-intact; out-of-cycle rotation to Q2b; retain-the-evidence - the zeroize branch fires destroyEvidence on the KG-01 unit log, which the campaign record remembers. S22 "Connecting the Dots": the attribution report. The Campaign Record panel and the audit log carry S17-S21 forward; the KG-01 line is present only if S21 kept it, and attribute-the-actor grades on evidence-chain-intact - one actor, or the recorded gap at a cost. The referral beat is Charlie's bridge to campaign 4. S23 "Dark Passes": spoof, SAR-2 burn (spaceEvents initialTle) and a jammer in the window, eight minutes before a pass that must deliver PLD-SAFE. triage is graded on the clock (ephemeris first while uplink-jammed is false); the denial is called with the reference in holdover. S24 "North Atlantic Storm": the capstone. Contact plan across both sites, rain then sleet, SAR-1 under the terrestrial carrier (notch), SAR-2 from Shetland on the post-burn set with the spoof on SH-02, SAR-3 commanded through the sleet with the denial and the hop; incident log and handover. 20 objectives, 10 clips (density gate allows 10 for the capstone). All four registered (campaign data 905-940 min, scenario registry, board sweep 17, phase-3 validation with fact tables per decision) and proven by their full-completion specs.
Phase 18 E, the spacecraft half. settings.telemetry starts TelemetryManager and a read-only Telemetry tab: channels grouped by subsystem with nominal values, noise, yellow/red limit bands and scripted excursions on the mission clock. Frames flow only while an antenna on the station is locked on the bird or pointed within tolerance of it (a manually pointed dish on a GEO slot); a dropped link freezes the last values and reads STALE. An excursion can end on a TT&C command ACK (endsOnCommandId) and recover over recoverySeconds, so a command is verifiable in the channel it was sent for. Conditions telemetry-frames-received, telemetry-channel-in-band (channel + band, observation-gated on the telemetry tab), telemetry-soh-nominal and ranging-measurements; facts soh-red-limit, soh-yellow-limit, telemetry-stale. Ranging: settings.commanding.ranging adds a TONE control to the TT&C console; each tone is gated like a command and, when it ACKs, records the true slant range to the target (CommandRecord.rangeKm/elapsedS).
TALON-2 (90071): the unit's own X-band MILSATCOM relay at 110.0 W, the bird behind the protected band, with SANDSTORM as its TT&C backup while the primary terminal is down. The contractor's arc (the campaign-2 bridge): S3 "First Shift": point the monitor, take telemetry, read EPS/TCS/ADCS/COMM/ OBC in band, call state of health (decision graded on soh-yellow-limit and telemetry-stale), one housekeeping dump, safe, log. 12 objectives, 100 pts. S4 "State of Health": a wheel transient that self-clears and a battery heater stuck on after eclipse exit; DEGRADED called with the channel named; TCS-HTR-OFF inside the window; verification read in the telemetry, not the ACK (the excursion ends on the command). 10 objectives, 100 pts. S5 "Ranging Pass": a six-tone ranging arc around an east-west station- keeping burn (spaceEvents initialTle on a GEO bird), the burn read as a ~1.5 km step in slant range with pointing steady, EPHEMERIS STALE, the post-burn set loaded, the arc handed to the engineers. 10 objectives, 100 pts. Registered in the ccs campaign (100-140 min), the scenario registry, and validation tests per scenario; full-completion specs for all three.
Phase 18 F closes the Signal Hunter arc. S2 Two Carriers: a terrestrial fixed-wireless sector arriving at the dish directly (3752 MHz, 1398 MHz IF, continuous) beside an uplink interferer through TP-1 (6001 MHz, 1374 MHz IF, 36 s on / 84 s off). The correlator reports NO CORRELATION on the terrestrial carrier; the path decision is graded on two new evidence facts, transponder-interference-active and terrestrial-interference-active (the envelope read split by event path), with both carriers observed as evidence. Uplink carrier fixed inside 25 km, refined to 15 km, both dispositions filed. S3 Cold Trail: first carrier fixed and briefed to the field team as an ellipse; its envelope closes at T+1520 and the cold-trail call is gated on the new interference-event-ended condition; the second carrier appears at T+1800 from ~80 km away; CLEAR, retune, refix on its own captures; mobility and current-fix fields filed. S4 Prove It: the correlator calibrated on the Cannon AFB terminal's authorised carrier (fix inside 20 km of the surveyed site), CLEAR, hostile fixed alone inside 20 km, ellipse closed with the new geolocation-ellipse-within condition (graded on the console's own 95% ellipse, 20 km semi-major), the call graded on transponder-interference-active, basis and confidence fields filed. Engine: InterferenceManager.isAnyEventInEnvelopeVia(path) and hasEventEnded(id); the two facts; the two conditions; docs guide fact list. Tests: shared signal-hunter validation helpers solve each scenario's geometry headlessly to prove every fix and ellipse threshold, and check objective titles against every other objective's description. e2e specs for all three (31/31).
Phase 18 G closes the curriculum at 99/99 with four short single-purpose drills at Galway, chained from S16: no pass, no clips, no timers, 100 points each, SYSTEM quizzes and one graded decision where the topic is a call. S25 Site Orientation: Group Security's ten-stop sheet - segments, lifetime, decision ownership, the soft target, effects, consequence framing, the monitor-and-control path, why OT is not IT and where the boundaries are, the antenna pattern that let the 12 April carrier in, and what stays inside the fence. S26 Baseline Check: RX modem 1 staged at 1410 against the 1414 baseline with the vendor change in the audit log; the drift is read on the console, the call graded on config-drifted before anything is flagged, the baseline restored, the entries flagged, the account held, the access request confirmed before first transmit, the sweep logged. S27 Removable Media: a USB mount and an unsigned ACU firmware stage from an escorted technician; the call graded on config-drifted with the media entry as evidence, the wrong branch injecting the reboot it would have caused; verification path, transfer rule, and the OT reasoning. S28 Vendor Session: an unticketed remote session, an off-baseline outbound flow and an ACU read burst; the call graded on audit-anomaly-present with the session flagged as evidence; account cut, flows flagged, boundary and reach stated. Engine: DecisionConsequence.auditEvent gains timestampLabel; the density gate gains a drill tier for S25+. Shared drill setup in drills.ts. Validation test covers wiring, staging, grading and chains; e2e specs for all four (28/28).
All sim time now comes from SimClock: the render loop feeds real frame time and UPDATE runs in fixed 60 Hz steps. Run time drives equipment dynamics; scenario time (missionNowMs) pauses for the brief freeze, quizzes, failure and a hidden tab, so scheduled events no longer drift against the sky by each learner's reading and quiz time. All randomness draws from seeded Rng streams (seed from ?seed=, ScenarioData.seed, or the scenario id), so runs replay exactly. window.advanceClock is the single e2e time hook. determinism.test.ts gates frame-rate independence, seed replay and no Math.random.
The transit ran T+300..600 s from load, so a player reaching Confirm Predicted Onset late found it over and Sky Noise Rising never ticked. Weather events take startAfterObjectiveId; Sky Temp now includes the transit noise. scenario17-late-onset.spec.ts covers the slow-player case.
Dependency ReviewThe following issues were found:
|
render-template walks with Dirent types instead of stat-then-read. vo-manifest strips tags until none remain (output unchanged). Plugin manifests must carry a semver version, an identifier className and entry/localesDir paths inside the plugin, and codegen escapes angle brackets and line separators. i18n skips __proto__/constructor/prototype when merging bundles. Adds a test that a stuck pre-fix S17 checkpoint restores into a winnable run.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This pull request modernizes and hardens the project's CI/CD pipeline, improves developer experience, and enforces better code and commit hygiene. Key changes include switching from npm to pnpm for dependency management, updating and simplifying workflow steps, adding support for a private submodule, and introducing stricter commit message validation.
CI/CD Pipeline Modernization and Dependency Management:
npmtopnpmfor dependency installation and scripts throughout all GitHub Actions workflows, improving speed and consistency. Updated Node.js version to 24 and setup actions to latest versions. [1] [2] [3] [4] [5] [6] [7] [8] [9]actions/checkoutandactions/setup-nodeto their latest major versions, and disabled submodule recursion except where explicitly needed. [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11] [12]Private Submodule Integration:
src/privateas a git submodule for private code, with secure SSH key handling in deploy and preview workflows. [1] [2] [3]src/privateis excluded from open source builds and only included where appropriate. (F4f0eb36L4R4, [1] [2]Code Quality and Commit Hygiene:
.husky/commit-msghook to enforce a strict, conventional commit message format, improving commit history readability and automation..gitattributesfile to standardize line endings and file handling across platforms, preventing Windows/Unix line ending issues..eslintrcfile, as linting is now handled by Biome.Static Analysis and Linting Configuration:
.deepsource.tomlto refine test and exclude patterns, and to better reflect the project's structure and environments.Minor Improvements:
pnpminstead ofnpmwhere relevant.These changes collectively improve the project's maintainability, security, and developer experience by using modern tooling and enforcing stricter standards.## Description
Testing
npm test)pnpm run test:e2e)