feat: prestige reads current LoC; tiered CI; v0.1.6

Prestige loop
  - prestigeGain + canPrestige now read state.layers[primary].amount instead
    of .best. The threshold is "do you currently hold N LoC", not "have you
    ever held N LoC", and the gain reflects what's actually being traded.
  - resetLayer wipes best and total too. Each prestige is a fresh slate;
    nothing rides through to bias the next run's gain calculation.
  - PrestigeButton tooltip: "current/threshold" line and explanatory copy
    now reference current upstream amount.

CI
  - Tiered gating via job-level `if:`. Non-main branch pushes run only the
    pre-build set (lint, secrets-scan, vuln-scan, sast). Pull requests AND
    main pushes additionally run test + build-images + image-scan. Main
    pushes alone run the Harbor push + cosign + per-package git tags.
  - `needs:` cascades skipped upstream jobs to dependents, so we don't
    duplicate the condition on every downstream job.

Both package versions bumped to 0.1.6; patch notes entry added.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-17 22:44:35 -07:00
parent 0c79235c72
commit 7d3476f946
7 changed files with 61 additions and 17 deletions
+18
View File
@@ -1,11 +1,25 @@
name: CI
# Tiered CI:
# - non-main push: pre-build (lint, secrets-scan, vuln-scan, sast) only.
# Fast feedback while iterating on a feature branch.
# - pull_request: above + test + build-images + image-scan.
# Heavy verification before code lands.
# - push to main: above + push to Harbor + cosign + per-package git tags.
# Same set as PRs, plus the one job that actually publishes.
#
# Job-level `if:` is the only gate — `needs:` cascades a skipped upstream
# job to its dependents automatically, so we don't have to repeat conditions
# on every downstream job.
on:
push:
pull_request:
jobs:
test:
# PR-or-main-push tier.
if: ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }}
runs-on: ubuntu-latest
services:
@@ -210,6 +224,8 @@ jobs:
path: semgrep.sarif
build-images:
# PR-or-main-push tier. Skipped on non-main feature-branch pushes.
if: ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }}
runs-on: ubuntu-latest
needs: [test, lint, secrets-scan, vuln-scan, sast]
steps:
@@ -254,6 +270,8 @@ jobs:
retention-days: 1
image-scan:
# PR-or-main-push tier. Skipped on non-main feature-branch pushes.
if: ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }}
runs-on: ubuntu-latest
needs: build-images
steps:
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@teh-riehl/client",
"version": "0.1.5",
"version": "0.1.6",
"private": true,
"type": "module",
"scripts": {
@@ -24,7 +24,8 @@ interface Props {
*
* Disabled when the player doesn't meet the threshold. Hover for a full
* read-out: what gets reset, what gets gained, and where the threshold sits
* relative to the player's current best.
* relative to the player's CURRENT upstream amount (the resource that's
* actually being traded).
*/
export function PrestigeButton({ layer }: Props) {
const dispatch = useAppDispatch();
@@ -37,7 +38,7 @@ export function PrestigeButton({ layer }: Props) {
: 0,
);
const primaryId = layer.branches[0];
const primaryBest = useAppSelector((s) => (primaryId ? (s.layers[primaryId]?.best ?? 0) : 0));
const primaryAmount = useAppSelector((s) => (primaryId ? (s.layers[primaryId]?.amount ?? 0) : 0));
if (!layer.reset) return null;
@@ -81,7 +82,7 @@ export function PrestigeButton({ layer }: Props) {
<Row label="gain" value={`+${formatNumber(gain)} ${symbol}`.trim()} accent={can} />
<Row
label="threshold"
value={`${formatNumber(primaryBest)} / ${formatNumber(requirement)} ${primarySymbol}`.trim()}
value={`${formatNumber(primaryAmount)} / ${formatNumber(requirement)} ${primarySymbol}`.trim()}
accent={can}
/>
{Math.abs(requirement - baseReq) > 1e-9 && (
@@ -92,7 +93,7 @@ export function PrestigeButton({ layer }: Props) {
/>
)}
<p className="text-comment leading-snug">
// Gain scales with your best {primaryName} this run.
// Gain scales with your current {primaryName}. Best + total reset on prestige.
</p>
</div>
</TooltipContent>
@@ -69,16 +69,24 @@ const layersSlice = createSlice({
}
},
/** Reset a layer (used during prestige). */
/**
* Reset a layer (used during prestige). Wipes amount, best, total,
* and buyables back to zero. Milestones and achievements persist —
* they're permanent "you reached this once" markers, not run-state.
*
* `keepUpgrades` lets a milestone effect (e.g. "Ten in a Row") opt
* into keeping owned upgrades through the reset. Off by default.
*/
resetLayer(state, action: PayloadAction<{ layerId: string; keepUpgrades?: boolean }>) {
const layer = state[action.payload.layerId];
if (!layer) return;
layer.amount = 0;
layer.best = 0;
layer.total = 0;
layer.buyables = {};
if (!action.payload.keepUpgrades) {
layer.upgrades = [];
}
// milestones and achievements persist across normal resets
},
/**
@@ -22,24 +22,30 @@ export function effectiveRequirement(layer: Layer, state: RootState): number {
* Resolve the prestige currency the player would gain right now.
* Returns `null` when the layer has no reset config (i.e. not prestigeable).
*
* When a requirement multiplier is active, we divide the formula input by
* that multiplier so a `prestigePoints(req, pow)` formula stays in sync
* with the effective gate — the player isn't surprised by a "can prestige
* but gain is 0" window.
* Reads CURRENT amount (not best/total) so the gain is what the player is
* actually trading away this prestige — not a high-water mark that survived
* a previous run. Pairs with resetLayer() wiping best+total so each run is
* a clean slate.
*
* When a requirement multiplier is active (e.g. force-push), we divide the
* formula input by that multiplier so a `prestigePoints(req, pow)` formula
* stays in sync with the effective gate — the player isn't surprised by a
* "can prestige but gain is 0" window.
*/
export function prestigeGain(layer: Layer, state: RootState): number | null {
if (!layer.reset) return null;
const ctx = buildEvalContext(state.layers, state.game.elapsedMs);
const primary = layer.branches[0];
if (!primary) return 0;
const best = state.layers[primary]?.best ?? 0;
const amount = state.layers[primary]?.amount ?? 0;
const reqMult = crossLayerMultiplier(layer.id, 'requirement', undefined, ctx);
return evalFormula(layer.reset.gainFormula, best / reqMult, ctx);
return evalFormula(layer.reset.gainFormula, amount / reqMult, ctx);
}
/**
* Whether the player meets the requirement to perform a reset right now.
* Currently checks the primary upstream layer's amount against requirement.
* Whether the player can perform the reset right now. Compares CURRENT
* upstream amount (the resource the player is about to trade) against the
* effective requirement — not the upstream layer's best.
*/
export function canPrestige(layer: Layer, state: RootState): boolean {
if (!layer.reset) return false;
@@ -48,7 +54,7 @@ export function canPrestige(layer: Layer, state: RootState): boolean {
const requirement = effectiveRequirement(layer, state);
const primary = layer.branches[0];
if (!primary) return false;
return (state.layers[primary]?.best ?? 0) >= requirement;
return (state.layers[primary]?.amount ?? 0) >= requirement;
}
/**
+11
View File
@@ -10,6 +10,17 @@ export interface PatchNote {
* Vite/tsc can resolve them without bundler magic. Newest entry first.
*/
export const patchNotes: ReadonlyArray<PatchNote> = [
{
version: '0.1.6',
date: '2026-05-17',
body: [
'## v0.1.6 — Prestige loop, fresh slate',
'',
'- **Commit gain now scales with your CURRENT Lines of Code, not your best.** The threshold tooltip shows current/threshold, and pushing past your old best no longer matters once you prestige. Each run is its own trade.',
"- **`best` and `total` reset on prestige.** Past v0.1.5 builds let those high-water marks ride through resets — they don't any more. A fresh prestige is a fresh slate, the way the design intended.",
'- **CI is tiered** so feature branches get fast feedback: non-main pushes run only lint / secrets-scan / vuln-scan / SAST. PRs add unit + integration tests, image builds, and image scans. Main pushes add the Harbor push + cosign + per-package git tags. (Infra-only — no gameplay changes from the CI side.)',
].join('\n'),
},
{
version: '0.1.5',
date: '2026-05-17',
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@teh-riehl/server",
"version": "0.1.5",
"version": "0.1.6",
"private": true,
"scripts": {
"build": "nest build",