@@ -38,6 +38,8 @@ type Column = {
3838 kind : string ;
3939 sign : 1 | - 1 ;
4040 priority : number ;
41+ /** Whether this marginal basis is active at the reference goal vector. */
42+ activeAtReference : boolean ;
4143 flows : Flow [ ] ;
4244} ;
4345type FixedFlow = { item : string ; kind : string ; net : number } ;
@@ -256,9 +258,9 @@ function signedBoundary(
256258 * A multi-goal block is one coupled LP: probing each goal with every sibling at
257259 * zero can combine mutually incompatible recipe bases. Finite differences at
258260 * the solved full vector recover the active LP basis, including negative
259- * marginal flows when increasing one goal reduces another recipe. A separate
260- * zero-activity solve captures operational estimates such as incidental
261- * spoilage without mistaking a non-smooth recipe-basis change for fixed flow .
261+ * marginal flows when increasing one goal reduces another recipe. The affine
262+ * intercept makes the response pass through that full solved reference point,
263+ * retaining operational and activation flows that have zero local derivative .
262264 * Final apply validation catches a target that crosses into another basis. */
263265async function responseColumns (
264266 row : NonNullable < ReturnType < typeof q . getBlock > > ,
@@ -272,40 +274,81 @@ async function responseColumns(
272274 const baseResult = await computeBlock ( reference ) ;
273275 if ( baseResult . broken || baseResult . status !== "solved" ) return { columns : [ ] , fixed : [ ] } ;
274276 const base = signedBoundary ( reference , baseResult ) ;
275- const idle : SolveInput = {
276- ... reference ,
277- goals : reference . goals . map ( ( goal ) => ( {
278- ... goal ,
279- rate : 0 ,
280- direction : goalConsumes ( goal ) ? "consume" : "produce" ,
281- } ) ) ,
282- } ;
283- const idleResult = await computeBlock ( idle ) ;
277+ const anchorResult = reference . goals . some (
278+ ( goal , index ) => goal . rate !== doc . goals [ index ] ?. rate ,
279+ )
280+ ? await computeBlock ( doc )
281+ : baseResult ;
282+ const anchor =
283+ anchorResult . broken || anchorResult . status !== "solved"
284+ ? new Map < string , { kind : string ; net : number } > ( )
285+ : signedBoundary ( doc , anchorResult ) ;
284286 const columns = await Promise . all (
285287 reference . goals . map ( async ( goal , goalIndex ) : Promise < Column | null > => {
286288 const sign : 1 | - 1 = goalConsumes ( goal ) ? - 1 : 1 ;
287- const delta = Math . max ( 1e-3 , Math . abs ( goal . rate ) * 1e-3 ) ;
288- const perturbed : SolveInput = {
289- ...reference ,
290- goals : reference . goals . map ( ( candidate , index ) =>
291- index === goalIndex
292- ? { ...candidate , rate : candidate . rate + sign * delta }
293- : candidate ,
294- ) ,
289+ const solveBoundaryAt = async ( rate : number ) => {
290+ const perturbed : SolveInput = {
291+ ...reference ,
292+ goals : reference . goals . map ( ( candidate , index ) =>
293+ index === goalIndex ? { ...candidate , rate } : candidate ,
294+ ) ,
295+ } ;
296+ const result = await computeBlock ( perturbed ) ;
297+ if ( result . broken || result . status !== "solved" || result . unmade ?. includes ( goal . name ) )
298+ return null ;
299+ return signedBoundary ( perturbed , result ) ;
295300 } ;
296- const result = await computeBlock ( perturbed ) ;
297- if ( result . broken || result . status !== "solved" || result . unmade ?. includes ( goal . name ) )
298- return null ;
299- const next = signedBoundary ( perturbed , result ) ;
301+ let delta = Math . max ( 1e-3 , Math . abs ( goal . rate ) * 1e-3 ) ;
302+ let previous = base ;
303+ let next = await solveBoundaryAt ( goal . rate + sign * delta ) ;
304+ if ( ! next ) return null ;
305+
306+ // A sibling goal can already make more of this good than its own
307+ // minimum asks for. A tiny perturbation then consumes only that
308+ // existing surplus: total block output stays flat and the local
309+ // derivative is zero even though the goal becomes scalable after the
310+ // surplus is exhausted. Coupled goals can create the same plateau by
311+ // substituting one recipe basis for another. Search outward for the
312+ // next segment where the goal changes its own boundary flow, then
313+ // measure that segment locally so the plateau does not dilute its
314+ // true marginal response.
315+ const localOwnNet =
316+ ( ( next . get ( goal . name ) ?. net ?? 0 ) - ( base . get ( goal . name ) ?. net ?? 0 ) ) / delta ;
317+ const activeAtReference = sign * localOwnNet > EPS ;
318+ const coveredRate = sign * ( base . get ( goal . name ) ?. net ?? 0 ) ;
319+ if ( sign * localOwnNet <= EPS ) {
320+ let segmentMagnitude =
321+ Math . max ( Math . abs ( goal . rate ) , coveredRate , RATE_CHANGE_ABS_TOL ) * 1.001 ;
322+ for ( let attempt = 0 ; attempt < 6 ; attempt += 1 ) {
323+ const segmentDelta = Math . max ( 1e-3 , segmentMagnitude * 1e-3 ) ;
324+ const segmentStart = sign * segmentMagnitude ;
325+ const segmentBase = await solveBoundaryAt ( segmentStart ) ;
326+ const segmentNext = await solveBoundaryAt ( segmentStart + sign * segmentDelta ) ;
327+ const segmentOwnNet =
328+ segmentBase && segmentNext
329+ ? ( ( segmentNext . get ( goal . name ) ?. net ?? 0 ) -
330+ ( segmentBase . get ( goal . name ) ?. net ?? 0 ) ) /
331+ segmentDelta
332+ : 0 ;
333+ if ( segmentBase && segmentNext && sign * segmentOwnNet > EPS ) {
334+ previous = segmentBase ;
335+ next = segmentNext ;
336+ delta = segmentDelta ;
337+ break ;
338+ }
339+ segmentMagnitude *= 2 ;
340+ }
341+ }
342+
300343 const priority = doc . supplyPriorities ?. [ goal . name ] ?? doc . supplyPriority ?? 0 ;
301- const goods = new Set ( [ ...base . keys ( ) , ...next . keys ( ) ] ) ;
344+ const goods = new Set ( [ ...previous . keys ( ) , ...next . keys ( ) ] ) ;
302345 const flows : Flow [ ] = [ ...goods ] . flatMap ( ( good ) => {
303- const marginal = ( ( next . get ( good ) ?. net ?? 0 ) - ( base . get ( good ) ?. net ?? 0 ) ) / delta ;
346+ const marginal = ( ( next . get ( good ) ?. net ?? 0 ) - ( previous . get ( good ) ?. net ?? 0 ) ) / delta ;
304347 if ( Math . abs ( marginal ) <= EPS ) return [ ] ;
305348 return [
306349 {
307350 item : good ,
308- kind : next . get ( good ) ?. kind ?? base . get ( good ) ?. kind ?? "item" ,
351+ kind : next . get ( good ) ?. kind ?? previous . get ( good ) ?. kind ?? "item" ,
309352 role :
310353 marginal < 0
311354 ? "import"
@@ -325,46 +368,33 @@ async function responseColumns(
325368 kind,
326369 sign,
327370 priority,
371+ activeAtReference,
328372 flows,
329373 } ;
330374 } ) ,
331375 ) ;
332376 const solvedColumns = columns . filter (
333377 ( column ) : column is Column => column != null && ! FREE_GOODS . has ( column . good ) ,
334378 ) ;
335- const fixedBoundary =
336- idleResult . broken || idleResult . status !== "solved"
337- ? new Map < string , { kind : string ; net : number } > ( )
338- : signedBoundary ( idle , idleResult ) ;
339- if ( reference . goals . some ( ( goal ) => FREE_GOODS . has ( goal . name ) ) ) {
340- const withoutFree : SolveInput = {
341- ...reference ,
342- goals : reference . goals . map ( ( goal ) =>
343- FREE_GOODS . has ( goal . name )
344- ? {
345- ...goal ,
346- rate : 0 ,
347- direction : goalConsumes ( goal ) ? "consume" : "produce" ,
348- }
349- : goal ,
350- ) ,
351- } ;
352- const withoutFreeResult = await computeBlock ( withoutFree ) ;
353- if ( ! withoutFreeResult . broken && withoutFreeResult . status === "solved" ) {
354- const withoutFreeBoundary = signedBoundary ( withoutFree , withoutFreeResult ) ;
355- for ( const item of new Set ( [ ...base . keys ( ) , ...withoutFreeBoundary . keys ( ) ] ) ) {
356- const frozen = ( base . get ( item ) ?. net ?? 0 ) - ( withoutFreeBoundary . get ( item ) ?. net ?? 0 ) ;
357- if ( Math . abs ( frozen ) <= EPS ) continue ;
358- const current = fixedBoundary . get ( item ) ;
359- fixedBoundary . set ( item , {
360- kind :
361- base . get ( item ) ?. kind ??
362- withoutFreeBoundary . get ( item ) ?. kind ??
363- current ?. kind ??
364- "item" ,
365- net : ( current ?. net ?? 0 ) + frozen ,
366- } ) ;
367- }
379+ // fixed = anchor - J * anchor goals. This local affine intercept retains
380+ // flows that switch on with an active recipe but stay flat under tiny
381+ // perturbations; an idle-only intercept silently dropped them on every
382+ // re-linearization pass. A marginal basis found beyond a coproduct
383+ // plateau is excluded because that segment is not active at the current
384+ // reference point.
385+ const fixedBoundary = new Map (
386+ [ ...anchor ] . map ( ( [ item , flow ] ) => [ item , { ...flow } ] as const ) ,
387+ ) ;
388+ for ( const column of solvedColumns ) {
389+ if ( ! column . activeAtReference ) continue ;
390+ const anchorGoal = doc . goals . find ( ( goal ) => goal . name === column . good ) ;
391+ const magnitude = Math . abs ( anchorGoal ?. rate ?? 0 ) ;
392+ for ( const flow of column . flows ) {
393+ const current = fixedBoundary . get ( flow . item ) ;
394+ fixedBoundary . set ( flow . item , {
395+ kind : current ?. kind ?? flow . kind ,
396+ net : ( current ?. net ?? 0 ) - magnitude * flow . rate * ( flow . role === "import" ? - 1 : 1 ) ,
397+ } ) ;
368398 }
369399 }
370400 const fixed = [ ...fixedBoundary ] . flatMap ( ( [ item , flow ] ) =>
0 commit comments