Skip to content

Commit ae593be

Browse files
committed
fix(solver): derive rates from stock goals
1 parent c4c7435 commit ae593be

7 files changed

Lines changed: 30 additions & 4 deletions

File tree

app/src/db/queries.server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1911,7 +1911,7 @@ function recipeSignature(name: string): string {
19111911
* save time; a mismatch on a later check means the block's own inputs changed. */
19121912
/** Bumped when solver semantics change — folded into the reference fingerprint
19131913
* so every cache from an older solver reads stale and re-solves on first touch. */
1914-
const SOLVER_VERSION = "sv3";
1914+
const SOLVER_VERSION = "sv4";
19151915

19161916
export function blockReferenceFingerprint(data: BlockData): string {
19171917
const parts: string[] = [`S ${SOLVER_VERSION}`];

app/src/lib/goals.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@ describe("normalizeBlockData", () => {
5151
expect(normalizeBlockData(data)).toEqual(data);
5252
});
5353

54+
it("repairs a stale stock-goal rate from its amount and refill window", () => {
55+
expect(
56+
normalizeBlockData({
57+
goals: [{ name: "belt", rate: 1, stock: 100, window: 600 }],
58+
}).goals,
59+
).toEqual([{ name: "belt", rate: 1 / 6, stock: 100, window: 600 }]);
60+
});
61+
5462
it("yields an empty goal list for an empty block", () => {
5563
expect(normalizeBlockData({ recipes: [] }).goals).toEqual([]);
5664
});

app/src/lib/goals.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export type RawBlockData = Partial<Omit<BlockData, "goals">> & {
2323
target?: string;
2424
rate?: number;
2525
extraGoals?: string[];
26-
goals?: Array<{ name: string; rate: number | null }>;
26+
goals?: Array<Omit<Goal, "rate"> & { rate: number | null }>;
2727
};
2828

2929
/** Migrate a possibly-legacy block doc to the `goals` shape. Idempotent: a doc that
@@ -33,7 +33,13 @@ export function normalizeBlockData<T extends RawBlockData>(d: T): BlockData {
3333
const { target, rate, extraGoals: _extraGoals, goals, ...rest } = d;
3434
if (Array.isArray(goals)) {
3535
// Drop any rate-less goals (old "unpinned co-products") — they're byproducts now.
36-
const kept = goals.filter((g): g is Goal => !!g.name && g.rate != null);
36+
const kept = goals
37+
.filter((g): g is Goal => !!g.name && g.rate != null)
38+
.map((g) => {
39+
if (g.stock == null) return g;
40+
const window = g.window ?? STOCK_WINDOW_DEFAULT;
41+
return { ...g, window, rate: g.stock / window };
42+
});
3743
return { ...(rest as object), goals: kept } as BlockData;
3844
}
3945
const next: Goal[] = [];

app/src/server/agent-tools-goals.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,14 @@ describe("submitBlock/submitPlan/buildingBill: multi-goal + keep-in-stock goals
121121
expect(res.goals).toEqual([{ name: "iron-plate", rate: 0.1, stock: 60, window: 600 }]);
122122
});
123123

124+
it("stock/window wins when a stale explicit rate is also present", async () => {
125+
const res = await draft({
126+
goals: [{ name: "iron-plate", rate: 1, stock: 100, window: 600 }],
127+
recipes: ["iron-plate"],
128+
});
129+
expect(res.goals).toEqual([{ name: "iron-plate", rate: 1 / 6, stock: 100, window: 600 }]);
130+
});
131+
124132
it("supports multiple goals in one block — a rate goal and a stock goal together", async () => {
125133
const res = await draft({
126134
goals: [

app/src/server/agent-tools.server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -772,7 +772,7 @@ function resolveGoals(input: {
772772
if (input.goals?.length) {
773773
return input.goals.map((g) => {
774774
const window = g.window ?? STOCK_WINDOW_DEFAULT;
775-
const rate = g.rate ?? g.stock! / window;
775+
const rate = g.stock != null ? g.stock / window : g.rate!;
776776
return { name: g.name, rate, ...(g.stock != null ? { stock: g.stock, window } : {}) };
777777
});
778778
}

docs/development/solver.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ from the Block balance Imports list rather than rendering the same contract twic
6363
A stock goal stores an amount and replenishment window. The compute layer converts it to
6464
`amount / window` for solving, then tags its persisted factory flow as stock replenishment
6565
rather than continuous output.
66+
Normalization derives that rate on every read, making `stock` plus `window` authoritative even
67+
if an older document also contains a stale explicit rate.
6668

6769
The first goal anchors the block's name, scaling controls, and default icon. Additional
6870
goals participate in the same solve without changing that identity.

docs/guide/blocks.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ Select **New block**, then use **+ goal** to choose an item or fluid.
2929
Right-click a goal and select **Keep in stock instead (buffer, not throughput)** when the
3030
intent is to refill a quantity over time rather than sustain a continuous rate. Stock-only
3131
production appears separately in Factory.
32+
The solver rate is always the stock amount divided by its refill window; an older saved rate
33+
cannot turn a buffer goal back into continuous production.
3234

3335
A negative goal is already the block's visible import contract, so Block balance does not
3436
repeat that good under **Imports**. Other ingredients the block needs still appear there.

0 commit comments

Comments
 (0)