## Boot-time environment report for ModularBot. ## ## The user runs the bot behind the SERVER/GUI, so the bot inherits the ## SERVER's environment — not the environment of whatever terminal they ## exported a variable in. This report answers, once per process at boot and ## in stdout, two questions: ## ## A. What TR_*/GUN_* variables did THIS process actually receive? (plus the ## process identity — pid, parent pid, parent command line — so "I exported ## it in the wrong terminal" is obvious at a glance). ## B. For every documented knob, what value will the bot actually use and did ## it come from the env or from the shipped default? ## ## Every line starts with `[env]` so the whole interface is: ## ## grep '^\[env\]' /tmp/modularbot_stdout.log ## ## It runs once (idempotent guard) and can be suppressed with TR_ENV_REPORT=0. ## It deliberately does NOT dump the whole environment: only TR_*/GUN_* are ## printed, because unrelated variables may hold secrets or machine paths. import std/[os, strutils, algorithm, times, sets] import env_dotenv import gun_harness/virtual_bullets import gun_harness/selector import movements/ram_decision import movements/the_floor_is_lava import movements/the_floor_is_lava_ring import movements/strafe import movements/wave_surfer import movements/learned_surfer import guns/tm_horizon import guns/bitbrain_gun import guns/pattern_matcher import vbullet_draw const EnvReportEnableEnvVar* = "TR_ENV_REPORT" # ── resolved values that live in ModularBot.nim (not importable here) ───────── type EnvReportContext* = object ## Everything the report needs that ModularBot owns. Values are the ## RESOLVED process-start values, not re-reads, so the report cannot ## diverge from what the bot will actually use. movementName*: string vBulletAdmitOnly*: bool vBulletDebug*: bool vBulletDebugGun*: string vBulletDebugMax*: int recordWorldState*: bool radarForceSpin*: bool radarScanLog*: bool radarScanLogPath*: string trackerProbe*: bool trackerProbePath*: string powerLog*: bool resultLog*: bool gunStatsPath*: string shotLogPath*: string disabledGuns*: HashSet[int] tmHorizon*: TmHorizonGun bitbrain*: BitBrainGun patternMatcher*: PatternMatcherGun # ── helpers ────────────────────────────────────────────────────────────────── proc onOff(b: bool): string {.inline.} = (if b: "on" else: "off") proc metricName(m: BulletMetric): string = case m of bmPoint: "point" of bmPath: "path" proc selectorModeName(m: SelectorMode): string = case m of smAbsolute: "absolute" of smRelative: "relative" proc rankName(r: RankStat): string = ## The word `GUN_SELECTOR_RANK` accepts on the command line, not the enum. case r of rsMean: "mean" of rsWilson: "wilson" of rsUCB: "ucb" of rsThompson: "thompson" of rsShrunk: "shrunk" proc tieBreakName(t: TieBreakMode): string = case t of tbOff: "off" of tbPoint: "point" of tbPointCommit: "pointCommit" proc sourceOf(name: string): string = ## Parsed-value knobs go through `getEnv(name, "")`; an empty value means the ## shipped default, so a non-empty value is the only thing that counts as ## "from the env". A value that came from the resolved .env file is labelled ## `.env` even though the file applied it with putEnv (so `getEnv` finds it). if isFromEnvFile(name): ".env" elif getEnv(name, "").len > 0: "env" else: "default" proc sourceOfPresence(name: string): string = ## Flags read with `existsEnv` are "from the env" whenever they are present, ## even if their value is "0" (presence is what the code tests). A value that ## came from the .env file is labelled `.env`. if isFromEnvFile(name): ".env" elif existsEnv(name): "env" else: "default" proc disabledGunsString(guns: HashSet[int]): string = if guns.len == 0: return "none" var ids: seq[int] for id in guns: ids.add id ids.sort() for i, id in ids: if i > 0: result.add "," result.add $id proc emit(name, value, source: string) = echo "[env] ", name, " = ", value, " (source: ", source, ")" proc readProcFile(path: string): string = ## Best-effort /proc read; never raises, returns "" when unavailable. try: readFile(path) except CatchableError: "" proc ppidFromStatus(): int = for line in readProcFile("/proc/self/status").splitLines(): if line.startsWith("PPid:"): let parts = line.splitWhitespace() if parts.len >= 2: try: return parseInt(parts[1]) except ValueError: return -1 -1 proc cmdlineOf(pid: int): string = ## NUL-separated argv from /proc//cmdline, flattened for display. if pid < 0: return "(unavailable)" let raw = readProcFile("/proc/" & $pid & "/cmdline") if raw.len == 0: return "(unavailable)" result = raw.replace('\0', ' ').strip() if result.len == 0: result = "(available but empty)" proc selfCmdline(): string = let raw = readProcFile("/proc/self/cmdline") if raw.len == 0: return getAppFilename() result = raw.replace('\0', ' ').strip() if result.len == 0: result = getAppFilename() # ── section A: the raw process environment + process identity ──────────────── proc printRawEnvironment() = echo "[env] --- A. raw process environment (what THIS process really has) ---" var rawVars: seq[(string, string)] var total = 0 for key, val in envPairs(): inc total if key.startsWith("TR_") or key.startsWith("GUN_"): rawVars.add (key, val) rawVars.sort(proc(a, b: (string, string)): int = cmp(a[0], b[0])) for (key, val) in rawVars: echo "[env] ", key, "=", val echo "[env] raw: ", rawVars.len, " TR_*/GUN_* of ", total, " total environment variables" if rawVars.len == 0: echo "[env] WARNING: this process has NO TR_*/GUN_* variables - the env you " & "exported did NOT reach the bot. See docs/env_reference.md." # Process identity. The parent is the process that SPAWNED the bot: while # running under the GUI it is the SERVER/GUI, never the user's interactive # shell — which is exactly why exporting in an unrelated terminal does nothing. let pid = getCurrentProcessId() let ppid = ppidFromStatus() echo "[env] pid=", pid, " ppid=", ppid echo "[env] cwd=", getCurrentDir() echo "[env] self_cmd=", selfCmdline() echo "[env] parent_cmd=", cmdlineOf(ppid) # ── section B: effective values ────────────────────────────────────────────── proc printEffectiveValues(ctx: EnvReportContext) = echo "[env] --- B. effective values (resolved at boot) ---" # ── movement / process-level ModularBot knobs ───────────────────────────── # Only `tfil_ring` and `strafe` select a non-default engine; every other value # runs `tfil`, so report the RESOLVED engine rather than the raw string. let effectiveMovement = if ctx.movementName == "tfil_ring": "tfil_ring" elif ctx.movementName == "strafe": "strafe" elif ctx.movementName == "surf": "surf" elif ctx.movementName == "learned": "learned" else: "tfil" emit("TR_MOVEMENT", effectiveMovement, sourceOf("TR_MOVEMENT")) emit("TR_MOVEMENT_LOG", onOff(MovementLog), sourceOfPresence("TR_MOVEMENT_LOG")) emit("TR_VBULLET_ADMIT_ONLY", onOff(ctx.vBulletAdmitOnly), sourceOf("TR_VBULLET_ADMIT_ONLY")) emit("TR_VBULLET_DEBUG", onOff(ctx.vBulletDebug), sourceOfPresence(VBulletDebugEnv)) emit("TR_VBULLET_DEBUG_GUN", (if ctx.vBulletDebugGun.len == 0: "(unset -> selected gun)" else: ctx.vBulletDebugGun), sourceOf(VBulletDebugGunEnv)) emit("TR_VBULLET_DEBUG_MAX", $ctx.vBulletDebugMax, sourceOf(VBulletDebugMaxEnv)) emit("TR_POWER_LOG", onOff(ctx.powerLog), sourceOfPresence("TR_POWER_LOG")) emit("TR_RESULT_LOG", onOff(ctx.resultLog), sourceOf("TR_RESULT_LOG")) emit("TR_RECORD_WORLDSTATE", onOff(ctx.recordWorldState), sourceOfPresence("TR_RECORD_WORLDSTATE")) emit("TR_RADAR_FORCE_SPIN", onOff(ctx.radarForceSpin), sourceOfPresence("TR_RADAR_FORCE_SPIN")) emit("TR_RADAR_SCANLOG", onOff(ctx.radarScanLog), sourceOfPresence("TR_RADAR_SCANLOG")) emit("TR_RADAR_SCAN_LOG_PATH", ctx.radarScanLogPath, sourceOf("TR_RADAR_SCAN_LOG_PATH")) emit("TR_TRACKER_PROBE", onOff(ctx.trackerProbe), sourceOfPresence("TR_TRACKER_PROBE")) emit("TR_TRACKER_PROBE_PATH", ctx.trackerProbePath, sourceOf("TR_TRACKER_PROBE_PATH")) emit("GUN_STATS_PATH", ctx.gunStatsPath, sourceOf("GUN_STATS_PATH")) emit("GUN_SHOTLOG_PATH", ctx.shotLogPath, sourceOf("GUN_SHOTLOG_PATH")) let seed = getEnv("GUN_SELECTOR_SEED", "") if seed.len > 0: emit("GUN_SELECTOR_SEED", seed, "env") else: emit("GUN_SELECTOR_SEED", "(unset -> time+pid)", "default") # ── rack ────────────────────────────────────────────────────────────────── emit("GUN_RACK_DISABLE", disabledGunsString(ctx.disabledGuns), sourceOf("GUN_RACK_DISABLE")) for i in 0.. 0.0), sourceOf("TR_TFIL_PILLAR_ON")) # ── movement (STRAFE) ───────────────────────────────────────────────────── emit("TR_STRAFE_BAND", $StrafeBand, sourceOf("TR_STRAFE_BAND")) emit("TR_STRAFE_SPREAD", $StrafeSpread, sourceOf("TR_STRAFE_SPREAD")) emit("TR_STRAFE_REACH", $StrafeReach, sourceOf("TR_STRAFE_REACH")) emit("TR_STRAFE_DWELL_MIN", $StrafeDwellMin, sourceOf("TR_STRAFE_DWELL_MIN")) emit("TR_STRAFE_DWELL_MAX", $StrafeDwellMax, sourceOf("TR_STRAFE_DWELL_MAX")) emit("TR_STRAFE_LOG", onOff(StrafeLog), sourceOfPresence("TR_STRAFE_LOG")) emit("TR_STRAFE_RANGE", $StrafeRange, sourceOf("TR_STRAFE_RANGE")) emit("TR_STRAFE_RANGE_TOL", $StrafeRangeTol, sourceOf("TR_STRAFE_RANGE_TOL")) emit("TR_STRAFE_TILT_MAX", $StrafeTiltMax, sourceOf("TR_STRAFE_TILT_MAX")) emit("TR_STRAFE_TILT_GAIN", $StrafeTiltGain, sourceOf("TR_STRAFE_TILT_GAIN")) # curved wings + wall escape (j112) emit("TR_STRAFE_KAPPA", $StrafeKappa, sourceOf("TR_STRAFE_KAPPA")) emit("TR_STRAFE_WALL_MARGIN", $StrafeWallMargin, sourceOf("TR_STRAFE_WALL_MARGIN")) emit("TR_STRAFE_WING_MAX", $StrafeWingMax, sourceOf("TR_STRAFE_WING_MAX")) emit("TR_STRAFE_WALL_BIAS", $StrafeWallBias, sourceOf("TR_STRAFE_WALL_BIAS")) emit("TR_STRAFE_WALL_SAFE", $StrafeWallSafe, sourceOf("TR_STRAFE_WALL_SAFE")) emit("TR_STRAFE_ESCAPE", onOff(StrafeEscape), sourceOf("TR_STRAFE_ESCAPE")) emit("TR_STRAFE_FIRE_FIX", onOff(StrafeFireFix), sourceOf("TR_STRAFE_FIRE_FIX")) # j134: the global switch the shared `movement_harness/fire_tracker` reads. # Every mover's effective value is this AND (for STRAFE) TR_STRAFE_FIRE_FIX. emit("TR_FIRE_FIX", onOff(TfilFireFix), sourceOf("TR_FIRE_FIX")) # j134 TASK B diagnostic (off by default): per-reading tick/raw/correction trace. emit("TR_FIRE_DIAG", onOff(StrafeFireDiag), sourceOf("TR_FIRE_DIAG")) emit("TR_STRAFE_HEAT_GRID", onOff(StrafeHeatGrid), sourceOf("TR_STRAFE_HEAT_GRID")) # STRAFE's heat shape is its own RETUNE (bullet 20/10, corridor 10, wall # 15/5), override-able per run so the shipped field can be A/B'd on one # binary at boot. emit("TR_STRAFE_BULLET_CORE", $StrafeBulletCore, sourceOf("TR_STRAFE_BULLET_CORE")) emit("TR_STRAFE_BULLET_AURA", $StrafeBulletAura, sourceOf("TR_STRAFE_BULLET_AURA")) emit("TR_STRAFE_CORRIDOR_HEAT", $StrafeCorridorHeat, sourceOf("TR_STRAFE_CORRIDOR_HEAT")) emit("TR_STRAFE_WALL_HOTNESS", $StrafeWallHotness, sourceOf("TR_STRAFE_WALL_HOTNESS")) emit("TR_STRAFE_WALL_RADIANCE", $StrafeWallRadiance, sourceOf("TR_STRAFE_WALL_RADIANCE")) # ── movement (WAVE SURFER) ──────────────────────────────────────────────── emit(SurfPrefDistEnv, $SurfPrefDist, sourceOf(SurfPrefDistEnv)) emit(SurfDistBandEnv, $SurfDistBand, sourceOf(SurfDistBandEnv)) emit(SurfWallMarginEnv, $SurfWallMargin, sourceOf(SurfWallMarginEnv)) emit(SurfRadialFracEnv, $SurfRadialFrac, sourceOf(SurfRadialFracEnv)) emit(SurfLogEnv, onOff(SurfLog), sourceOfPresence(SurfLogEnv)) # ── ramming ─────────────────────────────────────────────────────────────── emit("TR_RAM_OPPORTUNITY", onOff(RamOppEnabled), sourceOf("TR_RAM_OPPORTUNITY")) emit("TR_RAM_OPP_DIST", $RamOppDist, sourceOf("TR_RAM_OPP_DIST")) emit("TR_RAM_OPP_MARGIN", $RamOppMargin, sourceOf("TR_RAM_OPP_MARGIN")) emit("TR_RAM_ABORT_DMG", $RamAbortDmg, sourceOf("TR_RAM_ABORT_DMG")) emit("TR_RAM_PLAN", onOff(RamPlanEnabled), sourceOf("TR_RAM_PLAN")) emit("TR_RAM_PLAN_DIST", $RamPlanDist, sourceOf("TR_RAM_PLAN_DIST")) emit("TR_RAM_PLAN_MARGIN", $RamPlanMargin, sourceOf("TR_RAM_PLAN_MARGIN")) emit("TR_RAM_PLAN_HITRATE", $RamPlanHitRate, sourceOf("TR_RAM_PLAN_HITRATE")) emit("TR_RAM_LOG", onOff(RamLog), sourceOfPresence("TR_RAM_LOG")) # ── the horizon TM gun ──────────────────────────────────────────────────── # `resetLearning`/`targetChanged` resolve the lazily-read fields at round # start; force them here on a COPY so the report reads real resolved values # without touching the live gun's state. var tmh = ctx.tmHorizon discard tmh.targetChanged(-1) emit("TR_TMHORIZON_SHIFT", $tmh.shiftDeg, sourceOf("TR_TMHORIZON_SHIFT")) emit("TR_TMHORIZON_BIG_MULT", $tmh.bigMult, sourceOf("TR_TMHORIZON_BIG_MULT")) emit("TR_TMHORIZON_LOG", onOff(tmh.logEnabled), sourceOfPresence("TR_TMHORIZON_LOG")) emit("TR_TMHORIZON_RESET_ON_TARGET", onOff(tmh.resetOnTarget), sourceOf("TR_TMHORIZON_RESET_ON_TARGET")) emit("TR_TMHORIZON_NSTATES", $tmh.nStates, sourceOf("TR_TMHORIZON_NSTATES")) emit("TR_TMHORIZON_WINDOW", $tmh.windowN, sourceOf("TR_TMHORIZON_WINDOW")) emit("TR_TMHORIZON_RESET_DROP", $tmh.resetDrop, sourceOf("TR_TMHORIZON_RESET_DROP")) emit("TR_TMHORIZON_ACCURVE", onOff(tmh.accurveEnabled), sourceOfPresence("TR_TMHORIZON_ACCURVE")) emit("TR_TMHORIZON_RETRAIN_EVERY", $tmh.retrainEvery, sourceOf("TR_TMHORIZON_RETRAIN_EVERY")) emit("TR_TMHORIZON_EPOCHS", $tmh.retrainEpochs, sourceOf("TR_TMHORIZON_EPOCHS")) # ── the BitBrain ADE+SBC corrector ──────────────────────────────────────── # All knobs are resolved at gun construction (`initBitBrainGun`), so the # context holds the real values. The network itself is only built on the first # `predict`, which is why `[rack]` membership is the actual enable switch. emit("TR_BITBRAIN_MEM", memModeName(ctx.bitbrain.memMode), sourceOf(BB_MEM_ENV)) emit("TR_BITBRAIN_GAINS", bbGainsString(ctx.bitbrain.cands), sourceOf(BB_GAINS_ENV)) emit("TR_BITBRAIN_N", $ctx.bitbrain.nClasses, sourceOf(BB_N_ENV)) emit("TR_BITBRAIN_NADE", $ctx.bitbrain.nAde, sourceOf(BB_NADE_ENV)) emit("TR_BITBRAIN_RANGE", $ctx.bitbrain.maxDeg, sourceOf(BB_RANGE_ENV)) emit("TR_BITBRAIN_LOG", onOff(ctx.bitbrain.logEnabled), sourceOfPresence(BB_LOG_ENV)) emit("TR_BITBRAIN_MIN_OBS", $ctx.bitbrain.minObs, sourceOf(BB_MIN_OBS_ENV)) emit("TR_BITBRAIN_WARMUP", $ctx.bitbrain.warmupN, sourceOf(BB_WARMUP_ENV)) emit("TR_BITBRAIN_ADAPT", $ctx.bitbrain.adaptEvery, sourceOf(BB_ADAPT_ENV)) emit("TR_BITBRAIN_CALIB", $ctx.bitbrain.calibEvery, sourceOf(BB_CALIB_ENV)) emit("TR_BITBRAIN_DECAY", $ctx.bitbrain.decayEvery, sourceOf(BB_DECAY_ENV)) emit("TR_BITBRAIN_DECAY_FRAC", $ctx.bitbrain.decayFrac, sourceOf(BB_DECAY_FRAC_ENV)) emit("TR_BITBRAIN_SEED", $ctx.bitbrain.seed, sourceOf(BB_SEED_ENV)) emit("TR_BITBRAIN_RESET_ON_TARGET", onOff(ctx.bitbrain.resetOnTarget), sourceOf(BB_RESET_ON_TARGET_ENV)) # ── Pattern match-shape + radial knobs ──────────────────────────────────── # These are resolved lazily inside `predict` (which has not run at boot), so # unless something already forced them we report the raw env value and say so. if ctx.patternMatcher.paramConfigured: emit("TR_PATTERN_LEN", $ctx.patternMatcher.patternLen, sourceOf("TR_PATTERN_LEN")) emit("TR_PATTERN_DEPTH", $ctx.patternMatcher.histDepth, sourceOf("TR_PATTERN_DEPTH")) else: echo "[env] TR_PATTERN_LEN = ", getEnv("TR_PATTERN_LEN", $PatternLen), " (raw - not resolved here)" echo "[env] TR_PATTERN_DEPTH = ", getEnv("TR_PATTERN_DEPTH", $PatternDepth), " (raw - not resolved here)" if ctx.patternMatcher.radConfigured: emit("TR_PATTERN_RAD_OFFSET", $ctx.patternMatcher.radOffset, sourceOf("TR_PATTERN_RAD_OFFSET")) emit("TR_PATTERN_RAD_SCALE", $ctx.patternMatcher.radScale, sourceOf("TR_PATTERN_RAD_SCALE")) else: echo "[env] TR_PATTERN_RAD_OFFSET = ", getEnv("TR_PATTERN_RAD_OFFSET", "0.0"), " (raw - not resolved here)" echo "[env] TR_PATTERN_RAD_SCALE = ", getEnv("TR_PATTERN_RAD_SCALE", "1.0"), " (raw - not resolved here)" # ── the report's own switch ─────────────────────────────────────────────── emit(EnvReportEnableEnvVar, getEnv(EnvReportEnableEnvVar, "1"), sourceOf(EnvReportEnableEnvVar)) # ── build identity (which binary is the GUI actually running?) ─────────────── proc printBuildIdentity() = ## The user has been bitten by a stale binary. CompileDate/CompileTime are ## available at runtime (even if a reproducible-build toolchain pins them), ## and the binary path + mtime + size pin down the exact artifact. echo "[env] --- build identity ---" echo "[env] build nim=", NimVersion, " compileDate=", CompileDate, " compileTime=", CompileTime let bin = getAppFilename() var size = -1 var mtime = "unknown" if fileExists(bin): try: size = getFileSize(bin) mtime = $getLastModificationTime(bin) except CatchableError: discard echo "[env] build binary=", bin, " size=", size, " mtime=", mtime # ── Part 2: warnings about env mistakes ────────────────────────────────────── # # This is the point of the report. A typo must be LOUD, not a silent no-op. # Two mistakes are caught: # # 1. an unknown TR_*/GUN_* name (e.g. TR_TMHORIZON_NSTATE, missing S); # 2. a compile-time `{.intdefine.}`/`{.strdefine.}`/`{.booldefine.}` symbol # exported as an env var (e.g. `TMH_NSTATES=2`), which is NEVER read at # runtime and therefore does nothing. # # The known-name set is assembled from the modules' exported env-name # constants; the remaining inline reads are listed once here. The guard test # `common_libs/tests/test_env_report.nim` scans the tree for env reads and for # intdefine/strdefine/booldefine symbols and fails loudly if a name read # anywhere (or a new define) is missing from these tables, so neither can # silently drift. proc knownEnvNames*(): seq[string] = ## Every TR_*/GUN_* name this project reads at runtime. Order is irrelevant; ## callers put it in a set. Keep in sync with the tree scan in the guard test. result = @[ # exported constants — the single source of truth (do not re-spell these) MetricEnvVar, SelectorModeEnvVar, TieBreakEnvVar, RackShareEnvVar, PowerPolicyEnvVar, PowerFarDistEnvVar, PowerFarCapEnvVar, PowerMidCapEnvVar, PowerRefEnvVar, PowerEnergyHiEnvVar, PowerEnergyLoEnvVar, PowerEnergyMinEnvVar, PowerEnergyMaxEnvVar, PowerFinishKillEnvVar, PatternRadOffsetEnvVar, PatternRadScaleEnvVar, PatternLenEnvVar, PatternDepthEnvVar, TMH_SHIFT_ENV, TMH_BIG_MULT_ENV, TMH_LOG_ENV, TMH_RESET_ON_TARGET_ENV, TMH_WINDOW_ENV, TMH_RESET_DROP_ENV, TMH_NSTATES_ENV, TMH_ACCURVE_ENV, TMH_RETRAIN_EVERY_ENV, TMH_EPOCHS_ENV, BB_MEM_ENV, BB_GAINS_ENV, BB_N_ENV, BB_NADE_ENV, BB_RANGE_ENV, BB_LOG_ENV, BB_MIN_OBS_ENV, BB_WARMUP_ENV, BB_ADAPT_ENV, BB_CALIB_ENV, BB_DECAY_ENV, BB_DECAY_FRAC_ENV, BB_SEED_ENV, BB_RESET_ON_TARGET_ENV, # common_libs/bitbrain (counted-SBC library) reads these inline; job j102 "TR_BITBRAIN_MODE", "TR_BITBRAIN_DECAY_EVERY", "TR_BITBRAIN_DECAY_SHIFT", ] # rack names are constructed from the prefix + gun table, not spelled out for g in RackGunNames: result.add RackEnvPrefix & g # inline reads with no exported constant (the guard test scans for these) for n in [ "GUN_SELECTOR_WINDOW", "GUN_SELECTOR_MINOBS", "GUN_SELECTOR_TIE", "GUN_SELECTOR_FLOOR", "GUN_SELECTOR_POOL", "GUN_SELECTOR_RANK", "GUN_SELECTOR_SHRINK", "GUN_SELECTOR_DWELL", "GUN_SELECTOR_MARGIN", "GUN_SELECTOR_POINT_TIE", "GUN_SELECTOR_SEED", "GUN_RACK_DISABLE", "GUN_STATS_PATH", "GUN_SHOTLOG_PATH", "TR_MOVEMENT", "TR_MOVEMENT_LOG", "TR_RECORD_WORLDSTATE", "TR_RADAR_FORCE_SPIN", "TR_RADAR_SCANLOG", "TR_RADAR_SCAN_LOG_PATH", "TR_TRACKER_PROBE", "TR_TRACKER_PROBE_PATH", "TR_VBULLET_ADMIT_ONLY", VBulletDebugEnv, VBulletDebugGunEnv, VBulletDebugMaxEnv, "TR_POWER_LOG", "TR_RESULT_LOG", # the .env loader's own environment hook (TR_ENV_FILE names the file) "TR_ENV_FILE", "TR_RAM_OPPORTUNITY", "TR_RAM_OPP_DIST", "TR_RAM_OPP_MARGIN", "TR_RAM_ABORT_DMG", "TR_RAM_PLAN", "TR_RAM_PLAN_DIST", "TR_RAM_PLAN_MARGIN", "TR_RAM_PLAN_HITRATE", "TR_RAM_LOG", "TR_TFIL_RANGE_LO", "TR_TFIL_RANGE_HI", "TR_TFIL_RANGE_TEMP", "TR_TFIL_RANGE_K", "TR_TFIL_CORRIDOR_HEAT", "TR_TFIL_WALL_HOTNESS", "TR_TFIL_WALL_RADIANCE", "TR_TFIL_TILE_REPLAN", "TR_TFIL_COMMIT_TICKS", "TR_TFIL_NO_REV", "TR_TFIL_COMMIT_LOG", "TR_TFIL_HEAT_TIME", "TR_TFIL_HEAT_TAU", "TR_TFIL_HEAT_POWER_GAIN", "TR_TFIL_PILLAR_ON", "TR_STRAFE_BAND", "TR_STRAFE_SPREAD", "TR_STRAFE_REACH", "TR_STRAFE_DWELL_MIN", "TR_STRAFE_DWELL_MAX", "TR_STRAFE_LOG", "TR_STRAFE_RANGE", "TR_STRAFE_RANGE_TOL", "TR_STRAFE_TILT_MAX", "TR_STRAFE_TILT_GAIN", "TR_STRAFE_KAPPA", "TR_STRAFE_WALL_MARGIN", "TR_STRAFE_WING_MAX", "TR_STRAFE_WALL_BIAS", "TR_STRAFE_WALL_SAFE", "TR_STRAFE_ESCAPE", "TR_STRAFE_FIRE_FIX", "TR_FIRE_FIX", "TR_FIRE_DIAG", "TR_STRAFE_HEAT_GRID", "TR_STRAFE_BULLET_CORE", "TR_STRAFE_BULLET_AURA", "TR_STRAFE_CORRIDOR_HEAT", "TR_STRAFE_WALL_HOTNESS", "TR_STRAFE_WALL_RADIANCE", SurfPrefDistEnv, SurfDistBandEnv, SurfWallMarginEnv, SurfRadialFracEnv, SurfLogEnv, # movements/learned_surfer.nim (the state-conditional learned danger mover) LearnedDecayEveryEnv, LearnedDecayShiftEnv, LearnedAlphaEnv, LearnedTravelEnv, LearnedReversalEnv, LearnedPrefDistEnv, LearnedDistBandEnv, LearnedRadialFracEnv, LearnedWallMarginEnv, LearnedGlobalEnv, LearnedLabelEnv, LearnedRealEventsEnv, LearnedLogEnv, # harness vars (read by the test framework, inherited by the bot, so they # must NOT be reported as typos) "TR_SERVER_JAR", "TR_BATTLE_RUNNER", "TR_BATTLE_RUNNER_DIR", # the report's own switch EnvReportEnableEnvVar, ]: result.add n proc unknownEnvNames*(pairs: openArray[(string, string)]): seq[string] = ## Pure: every TR_*/GUN_* name in `pairs` the bot does not read, sorted. ## Anything else (e.g. PATH, JAVA_HOME) is deliberately ignored. let known = knownEnvNames().toHashSet() for (key, _) in pairs: if (key.startsWith("TR_") or key.startsWith("GUN_")) and key notin known: result.add key result.sort() type DefineMapping* = object ## A `{.intdefine.}`/`{.strdefine.}`/`{.booldefine.}` symbol and the runtime ## env var (if any) that actually controls the same knob. `suggestedEnv` is ## the env name a user would naturally guess, used only for the ## "does not exist" text when `runtimeEnv` is empty. defineName*: string runtimeEnv*: string suggestedEnv*: string ## Derived by grepping the tree for intdefine/strdefine/booldefine (see the ## guard test, which re-greps and fails if this table drifts): ## tm_horizon.nim:101,102,103,106,108,139 — TMH_* ; only NSTATES has an env ## tm_pattern.nim:50,53,55,57,59,62,65,68,69,85,87,92,94,96 — TM_* ## tsetlin.nim:29,116,118,119,120 — TM_* ## docs/env_reference.md documents the same table. const CompileTimeDefineMap*: seq[DefineMapping] = @[ # tm_horizon.nim — the ONLY define with a runtime twin (`TR_TMHORIZON_NSTATES`) DefineMapping(defineName: "TMH_NSTATES", runtimeEnv: "TR_TMHORIZON_NSTATES"), DefineMapping(defineName: "TMH_NCLAUSES", suggestedEnv: "TR_TMHORIZON_NCLAUSES"), DefineMapping(defineName: "TMH_S_DEF", suggestedEnv: "TR_TMHORIZON_S_DEF"), DefineMapping(defineName: "TMH_MIN_OBS", suggestedEnv: "TR_TMHORIZON_MIN_OBS"), DefineMapping(defineName: "TMH_STALE_MAX", suggestedEnv: "TR_TMHORIZON_STALE_MAX"), # tm_pattern.nim — no env form at all; do not invent a plausible one DefineMapping(defineName: "TM_NCLAUSES"), DefineMapping(defineName: "TM_NSTATES"), DefineMapping(defineName: "TM_MIN_OBS"), DefineMapping(defineName: "TM_CLASSES"), DefineMapping(defineName: "TM_CONF_MARGIN_DEF"), DefineMapping(defineName: "TM_SHRINK_DEF"), DefineMapping(defineName: "TM_GF_MODE"), DefineMapping(defineName: "TM_SOFT_BETA_DEF"), DefineMapping(defineName: "TM_RADIAL_RANGE_DEF"), DefineMapping(defineName: "TM_RAD_MARGIN_DEF"), DefineMapping(defineName: "TM_REV_TURN_DEG_DEF"), DefineMapping(defineName: "TM_REV_MARGIN_DEF"), DefineMapping(defineName: "TM_REV_GAIN_DEF"), # tsetlin.nim — no env form at all DefineMapping(defineName: "TM_N_CLAUSES"), DefineMapping(defineName: "TM_N_STATES"), DefineMapping(defineName: "TM_T_DEF"), DefineMapping(defineName: "TM_WINDOW_SIZE"), # TM_S_DEF is shared by tm_pattern.nim AND tsetlin.nim (one -d sets both) DefineMapping(defineName: "TM_S_DEF"), ] proc isCompileTimeDefine*(name: string): bool = ## True when `name` is one of our -d: symbols (never a runtime env var). for m in CompileTimeDefineMap: if m.defineName == name: return true proc setCompileTimeDefines*(pairs: openArray[(string, string)]): seq[string] = ## Pure: the -d: symbols present as env vars, sorted. for (key, _) in pairs: if isCompileTimeDefine(key): result.add key result.sort() proc defineWarningMessage*(defineName, value: string): string = ## The exact warning text for one -d: symbol set as an env var. "" when the ## name is not a known define. for m in CompileTimeDefineMap: if m.defineName != defineName: continue let head = "[env] WARNING: " & defineName & "=" & value & " is a COMPILE-TIME define (-d:" & defineName & "=" & value & ") and is NOT read at runtime; " if m.runtimeEnv.len > 0: return head & "the runtime equivalent is " & m.runtimeEnv & "=" & value & "." if m.suggestedEnv.len > 0: return head & m.suggestedEnv & " does not exist - this knob is compile-time only." return head & "no runtime env equivalent exists - this knob is " & "compile-time only." return "" proc printMistakeWarnings*() = ## The two warning paths, boot-only. Emits nothing when the env is clean, so ## a correct run keeps the documented A/B/build shape. var pairs: seq[(string, string)] for key, val in envPairs(): pairs.add (key, val) let unknown = unknownEnvNames(pairs) let defines = setCompileTimeDefines(pairs) if unknown.len == 0 and defines.len == 0: return echo "[env] --- warnings: misnamed variables ---" for key in unknown: echo "[env] WARNING: ", key, " is not a recognized TR_*/GUN_* variable; it is IGNORED. " & "Check the spelling (see docs/env_reference.md)." for key in defines: echo defineWarningMessage(key, getEnv(key, "")) # ── the entry point ────────────────────────────────────────────────────────── var envReportDone = false proc printEnvReport*(ctx: EnvReportContext) = ## Print the whole report exactly once per process, to stdout. Suppressed ## (with one visible note) by TR_ENV_REPORT=0/false/no/off. if envReportDone: return envReportDone = true let raw = getEnv(EnvReportEnableEnvVar, "").strip().toLowerAscii() if raw in ["0", "false", "no", "off"]: echo "[env] report suppressed by ", EnvReportEnableEnvVar, "=", raw, " (unset or 1 to enable)" return echo "[env] === ENVIRONMENT (boot report) ===" echo "[env] env file: ", (if envFilePath().len > 0: envFilePath() & " (source: " & envFileSource() & ")" else: "none") printRawEnvironment() printMistakeWarnings() printEffectiveValues(ctx) printBuildIdentity() echo "[env] === END ENVIRONMENT ==="