Resolve advisor conversion after battle losses before optional cleanup

Preserve committed casualties, payments and mandatory disposal when battle
losses leave BG bystanders alone. Bind the public phase-six receipt to the
original battle and resume its private winner/aftermath cursor once. Record
provisional priority; explosions with no surviving advisors create no response.

Repair stale Semuta fixtures to acknowledge the existing final battle window;
a representative failure reproduces against unchanged base035be51. Production
Semuta guards stay intact.

Validation: 16 affected files264/264, types/lint/build, five native battle and
GameTable SSR journeys, eight existing-profile response paths. Genuine44-action
sources earn original cards/forces and actual Collection outcomes. Broader
combinations and complete modes remain unfinished. Not deployed; no room/D1,
reset or restart operation. Runtime fingerprint
665defe51a1f46a6b5992b6e1e4b034203f91e7101acf860eb549f5bf9865151;
checkpoint prose/reference overlays are reviewed separately.
This commit is contained in:
Quintine committed 2026-10-24 20:37:09 +11:00
1 parent 035be51915
commit d83db397c0
17 files changed
+908 -26

No files matched your search

+54 -4
View File
@@ -28,6 +28,15 @@ export type AutomaticAdvisorGamont = {
mentat: boolean;
};
export type AutomaticAdvisorBattle = {
event: string;
turn: number;
territory: string;
combatants: string[];
winner: string | null;
result: 'normal' | 'traitor' | 'mutualTraitors' | 'explosion';
};
export type AutomaticAdvisorFlipSource = {
event: string;
owner: string;
@@ -39,6 +48,7 @@ export type AutomaticAdvisorFlipSource = {
| { producer: 'groundMovement'; mover: string; move: number }
| { producer: 'guildTransport'; transport: AutomaticAdvisorTransport }
| { producer: 'gamont'; gamont: AutomaticAdvisorGamont }
| { producer: 'battleLosses'; battle: AutomaticAdvisorBattle }
);
export type AutomaticAdvisorFlipPlayer = ForcePresence & {
@@ -60,6 +70,15 @@ export type AutomaticAdvisorFlipContext = AdvisorSettlementContext & MobileBoard
turn: number;
phase: number;
choamMentatPending?: boolean;
lastBattleContext?: {
event: string;
turn: number;
territory: string;
combatants: readonly string[];
winner: string | null;
result: string;
} | null;
battle?: unknown;
};
export class AutomaticAdvisorFlipError extends Error {
@@ -93,7 +112,7 @@ function otherPresence(ctx: AdvisorSettlementContext, player: AutomaticAdvisorFl
function currentWindow(ctx: AutomaticAdvisorFlipContext) {
return ctx.advanced === true && ctx.status === 'playing' &&
(ctx.phase === 5 || ctx.phase === 8) && positive(ctx.turn);
(ctx.phase === 5 || ctx.phase === 6 || ctx.phase === 8) && positive(ctx.turn);
}
/** A supported committed producer opens a response for these current groups.
@@ -199,6 +218,34 @@ function validateGamontSource(
'Automatic advisor conversion lost its final CHOAM Mentat opportunity.');
}
function validateBattleSource(
ctx: AutomaticAdvisorFlipContext,
source: Extract<AutomaticAdvisorFlipSource, { producer: 'battleLosses' }>,
) {
const receipt = source.battle;
requireFlip(record(receipt) &&
Object.keys(receipt).sort().join(',') === 'combatants,event,result,territory,turn,winner' &&
identifier(receipt.event) && positive(receipt.turn) && receipt.turn === source.turn &&
identifier(receipt.territory) && receipt.territory === source.territory &&
Array.isArray(receipt.combatants) && receipt.combatants.length === 2 &&
receipt.combatants.every(identifier) && new Set(receipt.combatants).size === 2 &&
receipt.combatants.every((id) => ctx.players.filter((player) => player.id === id).length === 1) &&
!receipt.combatants.includes(source.owner) &&
['normal', 'traitor', 'mutualTraitors', 'explosion'].includes(receipt.result) &&
(receipt.result === 'normal' || receipt.result === 'traitor'
? identifier(receipt.winner) && receipt.combatants.includes(receipt.winner)
: receipt.winner === null) &&
gameTerritories(ctx).some((place) => place.id === receipt.territory),
'Automatic advisor conversion needs the original public battle result and bystander group.');
const current = ctx.lastBattleContext;
requireFlip(!ctx.battle && record(current) && current.event === receipt.event &&
current.turn === receipt.turn && current.territory === receipt.territory &&
current.winner === receipt.winner && current.result === receipt.result &&
Array.isArray(current.combatants) && current.combatants.length === receipt.combatants.length &&
current.combatants.every((id, index) => id === receipt.combatants[index]),
'Automatic advisor conversion lost its original completed battle context.');
}
/** Validate the same committed producer and exact held group before either response outcome. */
export function validateAutomaticAdvisorFlip(
ctx: AutomaticAdvisorFlipContext,
@@ -210,10 +257,12 @@ export function validateAutomaticAdvisorFlip(
(source.producer === 'guildTransport' &&
Object.keys(source).sort().join(',') === 'event,forces,owner,phase,producer,territory,transport,turn') ||
(source.producer === 'gamont' &&
Object.keys(source).sort().join(',') === 'event,forces,gamont,owner,phase,producer,territory,turn')) &&
Object.keys(source).sort().join(',') === 'event,forces,gamont,owner,phase,producer,territory,turn') ||
(source.producer === 'battleLosses' &&
Object.keys(source).sort().join(',') === 'battle,event,forces,owner,phase,producer,territory,turn')) &&
identifier(source.event) && identifier(source.owner) && identifier(source.territory) &&
positive(source.turn) && source.turn === ctx.turn &&
source.phase === (source.producer === 'gamont' ? 8 : 5) &&
source.phase === (source.producer === 'gamont' ? 8 : source.producer === 'battleLosses' ? 6 : 5) &&
currentWindow(ctx) && source.phase === ctx.phase,
'Automatic advisor conversion lost its original current-turn committed producer.');
const owners = ctx.players.filter((player) => player.id === source.owner);
@@ -228,7 +277,8 @@ export function validateAutomaticAdvisorFlip(
requireFlip(movers.length === 1 && positive(source.move) && movers[0].moved === source.move,
'Automatic advisor conversion lost its original completed movement.');
} else if (source.producer === 'guildTransport') validateTransportSource(ctx, source);
else validateGamontSource(ctx, source);
else if (source.producer === 'gamont') validateGamontSource(ctx, source);
else validateBattleSource(ctx, source);
const forces = physicalGroup(owner, source.territory);
requireFlip(record(source.forces) && Object.keys(forces).length > 0 &&
Object.values(forces).every(positive) && Object.values(source.forces).every(positive) &&
+90 -4
View File
@@ -12,7 +12,7 @@ import { advancedMoritaniLeaderSkillsProfile, selectedDeckLeaderSkillsProfile, h
import { bribeTimingBlock, maximumBribe, type BribeOptions } from './bribe-options';
import { quoteAtreidesFullPlan } from './atreides-full-plan';
import { quoteTleilaxuSpecialKarama } from './tleilaxu-special-karama';
import { quoteAutomaticAdvisorFlips, validateAutomaticAdvisorFlip, AutomaticAdvisorFlipError, type AdvisorStance, type AutomaticAdvisorFlipSource, type AutomaticAdvisorTransport, type AutomaticAdvisorGamont } from './advisor-automatic-flip';
import { quoteAutomaticAdvisorFlips, validateAutomaticAdvisorFlip, AutomaticAdvisorFlipError, type AdvisorStance, type AutomaticAdvisorFlipSource, type AutomaticAdvisorTransport, type AutomaticAdvisorGamont, type AutomaticAdvisorBattle } from './advisor-automatic-flip';
import { quoteSpicePlacement, stormExposesTerritory, stormSectorAfter, wormConsumesForces } from './disaster-rules';
import { isStormCardDistance, type StormCardComponent } from './storm-cards';
import { createLeaderSkills, validateLeaderSkills, dealLeaderSkills, chooseLeaderSkill, returnDeadLeaderSkills, offerRevivedLeaderSkill, drawRevivedLeaderSkills, declineRevivedLeaderSkill, LeaderSkillError, type LeaderSkillsState, type LeaderSkillsView } from './leader-skills';
@@ -1265,6 +1265,11 @@ export type Game = {
pendingAutomaticAdvisorFlips?: AutomaticAdvisorFlipSource[];
pendingAutomaticAdvisorTransport?: AutomaticAdvisorTransport;
pendingAutomaticAdvisorGamont?: { receipt: AutomaticAdvisorGamont; card: string; nexusEvent?: string };
pendingAutomaticAdvisorBattle?: {
receipt: AutomaticAdvisorBattle;
resume: { kind: 'winner'; player: string; territory: string; cards: string[] } | { kind: 'aftermath' } | null;
signature?: string;
};
advisorPreparation?: AdvisorPreparationWindow;
ecazOccupyPreview?: boolean;
/** Fresh native Guild/Nexus payment-replacement profile, never inferred from a save. */
@@ -20292,13 +20297,19 @@ function currentAutomaticAdvisorFlip(g: Game, response: ResponseWindow) {
response.owner === source.owner && response.location === source.territory &&
response.advisorResume === undefined &&
JSON.stringify(g.pendingAutomaticAdvisorFlips?.[0]) === JSON.stringify(source),
'The mandatory advisor conversion has lost its original movement source.');
'The mandatory advisor conversion has lost its original committed source.');
if (source.producer === 'guildTransport')
requireRule(JSON.stringify(source.transport) === JSON.stringify(g.pendingAutomaticAdvisorTransport),
'The mandatory conversion has lost its original committed Guild transport.');
if (source.producer === 'gamont')
requireRule(JSON.stringify(source.gamont) === JSON.stringify(g.pendingAutomaticAdvisorGamont?.receipt),
'The mandatory conversion has lost its original completed Trip to Gamont.');
if (source.producer === 'battleLosses') {
const parent = g.pendingAutomaticAdvisorBattle;
requireRule(parent && JSON.stringify(source.battle) === JSON.stringify(parent.receipt),
'The mandatory conversion has lost its original completed battle.');
validateBattleAdvisorResume(g, parent);
}
automaticAdvisorRule(() => validateAutomaticAdvisorFlip(g, source));
return source;
}
@@ -20306,6 +20317,8 @@ function openAutomaticAdvisorFlip(g: Game) {
if (g.response || g.decision || g.truthtrance || g.phaseOpening) return;
const source = g.pendingAutomaticAdvisorFlips?.[0];
if (!source) return;
// Hold immediate losses through the original mandatory discard/casualty work.
if (source.producer === 'battleLosses' && !g.pendingAutomaticAdvisorBattle?.resume) return;
automaticAdvisorRule(() => validateAutomaticAdvisorFlip(g, source));
g.response = { kind: 'advisorFlip', owner: source.owner, location: source.territory,
advisors: false, passed: [], automaticAdvisorFlip: source,
@@ -20381,6 +20394,73 @@ function finishAutomaticAdvisorGamont(g: Game) {
if (parent.receipt.mentat)
g.decision = { kind: 'choamMentat', player: parent.receipt.player };
}
/** Capture actual surviving bystanders before settlement can auto-convert them. */
function prepareBattleAdvisorFlips(g: Game) {
const context = g.lastBattleContext;
if (!g.advanced || g.status !== 'playing' || g.phase !== 6 || g.battle ||
!context || context.turn !== g.turn || context.result === 'legacy') return;
if (g.pendingAutomaticAdvisorBattle) {
requireRule(g.pendingAutomaticAdvisorBattle.receipt.event === context.event,
'Finish the original battle conversion before another battle.');
return;
}
const groups = automaticAdvisorRule(() => quoteAutomaticAdvisorFlips(g))
.filter(group => group.territory === context.territory && !context.combatants.includes(group.player));
if (!groups.length) return;
requireRule(!g.pendingAutomaticAdvisorFlips?.length && !g.pendingAutomaticAdvisorTransport &&
!g.pendingAutomaticAdvisorGamont, 'Finish the original advisor conversion first.');
const receipt: AutomaticAdvisorBattle = { event: context.event, turn: context.turn,
territory: context.territory, combatants: [...context.combatants],
winner: context.winner, result: context.result };
// Private cleanup cards and cursor never enter the public source receipt.
g.pendingAutomaticAdvisorBattle = { receipt, resume: null };
for (const group of groups) {
const source: AutomaticAdvisorFlipSource = { event: crypto.randomUUID(), owner: group.player,
territory: group.territory, turn: g.turn, phase: g.phase, forces: group.forces,
producer: 'battleLosses', battle: receipt };
getPlayer(g, group.player).advisors![group.territory].aloneFlipHold = source.event;
(g.pendingAutomaticAdvisorFlips ??= []).push(source);
}
}
function validateBattleAdvisorResume(g: Game, parent: NonNullable<Game['pendingAutomaticAdvisorBattle']>) {
const resume = parent.resume;
requireRule(g.phase === 6 && !g.battle && parent.receipt.event === g.lastBattleContext?.event &&
resume && parent.signature === JSON.stringify(resume) && !g.pendingSukRescue &&
!g.pendingWinnerDiscards && !g.pendingTreacheryDiscard,
'Finish the original mandatory battle costs and casualties before advisor conversion.');
if (resume.kind === 'winner') {
const winner = getPlayer(g, resume.player);
requireRule(parent.receipt.winner === winner.id && resume.territory === parent.receipt.territory &&
new Set(resume.cards).size === resume.cards.length &&
resume.cards.every(id => winner.hand.some(card => card.id === id)),
'The advisor conversion lost the original winner cleanup cards.');
} else requireRule(resume.kind === 'aftermath', 'Resume only the original battle aftermath.');
}
/** Pause only at the original cleanup cursor; never rerun battle resolution. */
function openBattleAdvisorFlips(g: Game, resume: NonNullable<NonNullable<Game['pendingAutomaticAdvisorBattle']>['resume']>) {
prepareBattleAdvisorFlips(g);
const parent = g.pendingAutomaticAdvisorBattle;
if (!parent) return false;
requireRule(parent.resume === null && !!g.pendingAutomaticAdvisorFlips?.length &&
!g.response && !g.decision, 'The original battle conversion already owns its cleanup.');
parent.resume = structuredClone(resume);
parent.signature = JSON.stringify(parent.resume);
validateBattleAdvisorResume(g, parent);
openAutomaticAdvisorFlip(g);
return true;
}
function finishAutomaticAdvisorBattle(g: Game) {
const parent = g.pendingAutomaticAdvisorBattle;
if (!parent) return;
requireRule(!g.pendingAutomaticAdvisorFlips?.length,
'Finish all original advisor conversions before battle cleanup.');
validateBattleAdvisorResume(g, parent);
const resume = parent.resume!;
delete g.pendingAutomaticAdvisorBattle;
if (resume.kind === 'winner')
finishWinner(g, getPlayer(g, resume.player), resume.territory, resume.cards);
else finishBattle(g);
}
function applyAdvisorReleases(g: Game, released: AdvisorRelease[]) {
for (const entry of released)
delete getPlayer(g, entry.player).advisors?.[entry.territory];
@@ -22400,7 +22480,8 @@ function decideSharedSpice(g: Game, id: string, action: Action) {
function victory(g: Game, quote?: VictoryQuote) {
requireRule(!g.pendingAutomaticAdvisorFlips?.length && !g.pendingAutomaticAdvisorTransport &&
!g.pendingAutomaticAdvisorGamont, 'Resolve mandatory advisor conversions before checking victory.');
!g.pendingAutomaticAdvisorGamont && !g.pendingAutomaticAdvisorBattle,
'Resolve mandatory advisor conversions before checking victory.');
quote ??= currentVictoryQuote(g);
applyAdvisorReleases(g, quote.released);
g.winner = quote.winner;
@@ -25673,6 +25754,7 @@ function resolveBattle(g: Game, retreat?: DiplomatRetreatSelection) {
battleMandatoryDiscardSignature(g.lastBattleContext);
}
g.battle = null;
prepareBattleAdvisorFlips(g);
const cards = [...quote.winnerCards];
const continuation: Extract<
NonNullable<Game['pendingTreacheryDiscard']>['continuation'],
@@ -26149,6 +26231,7 @@ function projectedRihani(g: Game, owner: string) {
} : null };
}
function finishWinner(g: Game, winner: Player, t: string, cards: string[]) {
prepareBattleAdvisorFlips(g);
if (startRihaniVictory(g, winner, t, cards)) return;
const assassination = g.moritaniAssassinate?.opportunities.find(r => r.stage === 'skills');
if (assassination) {
@@ -26192,6 +26275,7 @@ function finishWinner(g: Game, winner: Player, t: string, cards: string[]) {
territory: t, optional: [...cards], commitment: structuredClone(pending) });
return;
}
if (openBattleAdvisorFlips(g, { kind: 'winner', player: winner.id, territory: t, cards })) return;
const context = g.lastBattleContext;
const moritani = byFaction(g, 'moritani');
if (g.advanced && context && moritani && !moritani.specialKaramaUsed &&
@@ -26411,6 +26495,7 @@ function projectedHomeworldVictoryReturn(g: Game, player: string) {
};
}
function finishBattle(g: Game) {
if (openBattleAdvisorFlips(g, { kind: 'aftermath' })) return;
const quote = currentBattleAftermathQuote(g);
for (const step of quote.steps) {
if (step.kind === 'transferTech') {
@@ -28217,13 +28302,14 @@ function finishResponse(g: Game, canceled: boolean, settlement?: BureaucratSettl
}
if (automaticAdvisorFlip) {
if (canceled)
log(g, `${bg.name}'s mandatory conversion in ${territory(to).name} was prevented. The completed ${automaticAdvisorFlip.producer === 'guildTransport' ? 'transport and payment stay' : automaticAdvisorFlip.producer === 'gamont' ? 'Trip to Gamont effect and card use stay' : 'movement stays'} committed; this group remains advisors while continuously alone.`,
log(g, `${bg.name}'s mandatory conversion in ${territory(to).name} was prevented. The completed ${automaticAdvisorFlip.producer === 'guildTransport' ? 'transport and payment stay' : automaticAdvisorFlip.producer === 'gamont' ? 'Trip to Gamont effect and card use stay' : automaticAdvisorFlip.producer === 'battleLosses' ? 'battle losses and costs stay' : 'movement stays'} committed; this group remains advisors while continuously alone.`,
{ faction: bg.faction, name: 'Mandatory advisor conversion prevented' });
g.pendingAutomaticAdvisorFlips!.shift();
if (!g.pendingAutomaticAdvisorFlips!.length) delete g.pendingAutomaticAdvisorFlips;
if (!g.pendingAutomaticAdvisorFlips?.length) {
finishAutomaticAdvisorTransport(g);
finishAutomaticAdvisorGamont(g);
finishAutomaticAdvisorBattle(g);
} else openAutomaticAdvisorFlip(g);
} else finishAdvisorReaction(g, response);
}
+1
View File
@@ -183,6 +183,7 @@ const cancellationSources = {
? { source: response.automaticAdvisorFlip, pending: g.pendingAutomaticAdvisorFlips?.[0] ?? null,
transport: response.automaticAdvisorFlip.producer === 'guildTransport' ? g.pendingAutomaticAdvisorTransport ?? null : null,
gamont: response.automaticAdvisorFlip.producer === 'gamont' ? g.pendingAutomaticAdvisorGamont?.receipt ?? null : null,
battle: response.automaticAdvisorFlip.producer === 'battleLosses' ? g.pendingAutomaticAdvisorBattle?.receipt ?? null : null,
group: g.players.find(p => p.id === response.owner)?.advisors?.[response.location ?? ''] ?? null }
: response.advisorPreparationSource
? { preparation: response.advisorPreparationSource, window: g.advisorPreparation ?? null }
+16 -6
View File
@@ -4315,6 +4315,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'A normal shipment enters as fighters unless joining existing advisors. Moving or shipping into your existing group must match its type. Advisors moved into an occupied territory without another Bene Gesserit group may remain peaceful or request a flip to fighters.',
'After Trip to Gamont completes during Mentat, the last outsider’s return or empty No-Field reveal can leave advisors alone. Karama can then prevent their conversion without undoing the completed effect or card use. The final CHOAM opportunity resumes afterward; victory waits for its original confirmation. Ordinary Mentat does not gain an extra final chooser.',
'A committed Guild cross-shipment or return to reserves can also leave advisors alone. Original transport permission, rate and any Guild income resolve first; payment, force transfer and shipment use stay committed if the later conversion is prevented. The ordinary ground move remains unused. Cross-shipment arrival follows the conversion once; a reserve return has no arrival.',
'After actual battle losses leave surviving bystanding advisors alone, Karama may prevent their conversion without undoing losses or costs. Mandatory card disposal, original skills and substitution finish first. Provisionally resolve this before optional winner-card cleanup and later aftermath, then resume the original cleanup once. An explosion that destroys the advisors creates no conversion.',
'Provisional cancellation duration: a prevented advisor group stays advisors while continuously alone, including across phase and turn changes. Another faction returning or the group becoming empty ends that held occurrence. Later arrivals joining that same group match its stance. Other non-ground automatic conversions and matching-group flip counters remain unfinished.',
'When another faction ships, moves or worm rides into your fighters, immediately choose whether to become advisors. The choice opens a Karama response. Repeated entry can offer a new choice.',
'After another faction’s off-planet shipment, choose a free force in the Polar Sink or an accompanying force at the shipment destination. An accompanying advisor group cannot flip to fighters in that turn while other factions remain.',
@@ -4326,7 +4327,7 @@ export const RULE_TOPICS: RuleTopic[] = [
{
"area": "Implementation",
"status": "Partial",
"detail": "Gamont now commits cost/return before phase8 conversion, keeps private fuel provenance out of its public event, and defers the original final chooser until the parent is consumed once. Victory waits for original confirmation; ordinary Mentat gains no chooser. Earlier ground/transport/preparation: Committed Guild cross/return now binds an exact public transport parent, holds the affected group before board benefits, preserves payment/shipment use, and resumes arrival once after conversion. Private projections/from-reserves do not create occurrences; legacy ground receipt shape remains. Earlier ground/preparation: Ground mandatory conversion is now prototyped: physical advisor arrival/last-outsider departure commits before an exact sourced Karama response. Shared normalization/board quotes retain a prevented continuously-alone stance. Non-ground producers and matching-group conversion remain unfinished. Earlier preparation: Shared native public-board eligibility wraps a current-turn receipt and exact owner/territory/turn source. Direct preparation resolves before-cost and outcome validation without entering movement from earlier phases. Native future declaration queues remain intact; successful shipment commits close the receipt, including markers and positive Sneak Attack.",
"detail": "Ground movement, committed Guild transport, Gamont and actual battle losses now have distinct event-bound mandatory-conversion sources. Shared settlement preserves prevented continuously-alone groups. Battle receipts select public identity/outcome only; a private cursor resumes original winner/aftermath after mandatory disposal, skills and substitution without replaying casualties. Transport retains paid transfer and one arrival; Gamont defers the original final chooser. Phases2–5 preparation retains its own window and per-territory prevention. Other automatic producers and matching-group counters remain unfinished.",
"evidence": [
"game/advisor-automatic-flip.ts",
"game/advisors.ts",
@@ -4341,7 +4342,7 @@ export const RULE_TOPICS: RuleTopic[] = [
{
"area": "Player controls",
"status": "Partial",
"detail": "Standard Gamont conversion response explains completed effect/card use and the pending victory check. Earlier controls: Transport conversion uses original Karama/Allow controls with completed-payment guidance; later real Intrusion remains its own decision. Earlier controls: Standard Karama/Allow responses show the completed ground move and explicit provisional continuous-alone duration, with no new confirmation or timer. Earlier preparation controls: Persistent owner-only territory actions and blocked/provisional guidance work in phases2–5 at quiet boundaries, including after initial decline. Busy and mandatory interactions stay blocked; private counts/cards/balances are not read. Original phase5 chooser remains.",
"detail": "Standard Karama/Allow controls explain completed movement, paid transport, Gamont card use or battle losses and the provisional continuously-alone hold. Battle guidance distinguishes committed casualties from remaining cleanup. Existing preparation controls remain available at quiet phases2–5 boundaries, including after initial decline; first actual shipment closes that window. No new confirmation or timing delay.",
"evidence": [
"docs/ADVISOR_AUTOMATIC_FLIPS.md",
"components/advisor-preparation.tsx",
@@ -4352,8 +4353,10 @@ export const RULE_TOPICS: RuleTopic[] = [
{
"area": "AI",
"status": "Partial",
"detail": "Eight existing-profile ordinary/final Gamont paths resolve legally before the actual final outcome, without strategy changes. Earlier paths: Eight existing-profile transport response paths stay legal without consuming ordinary movement or changing strategy. Earlier paths: Eight existing-profile ground-conversion paths accept legal standard responses and continue without a new strategy. Earlier preparation: The existing native preparation preference consumes the projected window; sixteen own-view existing-profile response paths accept16 legal proposals. No strategy tuning or difficulty calibration is claimed.",
"detail": "Eight existing-profile native winner-dial-all/mutual-Traitor response paths resolve legally to actual Collection without replay. Existing ground, transport, Gamont and preparation paths retain their separate evidence. No strategy refinement or difficulty calibration.",
"evidence": [
"tests/advisor-battle-flip-engine.test.ts",
"tests/fixture-advisor-battle-flip.ts",
"tests/advisor-gamont-flip-engine.test.ts",
"tests/advisor-transport-flip-engine.test.ts",
"tests/advisor-automatic-flip-engine.test.ts",
@@ -4364,7 +4367,7 @@ export const RULE_TOPICS: RuleTopic[] = [
{
"area": "Documentation",
"status": "Partial",
"detail": "Gamont scope distinguishes original-effect prevention, empty revelation, private fuel, final/ordinary timing and the unchanged provisional hold. Earlier contracts: Guild transport source/slot/payment and exactly-once arrival are distinguished from permission/rate; continuously-alone duration remains provisional. Earlier guidance: The ground-conversion contract distinguishes the printed counter, automatic setup, provisional continuous-alone duration and remaining producers. Earlier preparation: Supplied physical28 G and publisher timing distinguish the open window from shipment allowance flags. Denied-territory scope is provisional, existing Ecaz co-side composition is retained, and old pending/unknown-clock behavior and remaining forced-flip/combination limits are explicit.",
"detail": "Source physical17/28 E composes the group counter with mandatory conversion. Continuously-alone duration and battle cleanup priority are explicitly provisional. Public receipts, private continuation boundaries, native scenario provenance and remaining producers/combinations are distinguished. Preparation and supported Ecaz coexistence keep their separate contracts.",
"evidence": [
"docs/ADVISOR_AUTOMATIC_FLIPS.md",
"docs/ADVISOR_PREPARATION.md",
@@ -4375,8 +4378,10 @@ export const RULE_TOPICS: RuleTopic[] = [
{
"area": "Verification",
"status": "Partial",
"detail": "Gamont passes frozen18-file277/277, types/lint/build, five native Gamont/response/GameTable SSR journeys and eight profile paths. Genuine52/56-action sources earn two holds and a real final win versus next-turn outcome; empty revelation is pure-source evidence. Full No-Field/Nexus/Semuta and remaining producers remain open. Earlier proof: Transport passes frozen17-file243/243, types/lint/build, eight native transport/response/GameTable SSR journeys and eight existing-profile paths. Genuine36/42-action sources earn original Tueks5 and actual paid destination Intrusion. Affordable full-price return and unfunded cross remain distinct. Broader producers and final assurance stay open. Earlier proof: Ground conversion passes frozen16-file199/199, types/lint/build, five native move/response/GameTable SSR journeys and eight legal existing-profile paths. Genuine34/36-action sources retain originals; standard controls render without new callback/browser/deployed certification. Earlier preparation evidence: 16 files229/229, types/lint/build, twelve genuine new-control/gameplay/GameTable SSR journeys and sixteen minimal response paths pass. Public23/29/32/38/39-action sources preserve original setup advisor/Karama; a separate controlled ground/Terror unit proves positive Sneak Attack closure and zero nonclosure. Full combinations, visual/browser/deployed and comprehensive assurance remain open.",
"detail": "Battle conversion passes frozen16-file264/264, types/lint/build, five native battle/response/GameTable SSR journeys and eight existing-profile paths. Genuine44-action public sources earn original forces/cards and actual two-versus-zero city income; Lasgun/Shield destroys the advisor without a counter. Stale Semuta fixtures now submit existing final acknowledgements; production guards stay unchanged. Earlier ground/transport/Gamont/preparation evidence is in linked contracts. Full skill/Caladan/Face Dance/Semuta combinations, remaining producers, complete modes, browser/mobile/deployed and comprehensive assurance stay open.",
"evidence": [
"tests/advisor-battle-flip-engine.test.ts",
"tests/fixture-advisor-battle-flip.ts",
"tests/advisor-gamont-flip-engine.test.ts",
"tests/fixture-advisor-gamont-flip.ts",
"tests/advisor-transport-flip-engine.test.ts",
@@ -4391,7 +4396,12 @@ export const RULE_TOPICS: RuleTopic[] = [
"tests/moritani-force-leader.test.ts",
"tests/advisors.test.ts",
"tests/movement-phase-quote.test.ts",
"tests/karama-movement-cancellation.test.ts"
"tests/karama-movement-cancellation.test.ts",
"tests/fixture-semuta-battle-karama.ts",
"tests/fixture-semuta-homeworld-battle-loser.ts",
"tests/fixture-semuta-homeworld-battle-winner.ts",
"tests/fixture-semuta-homeworld-choam.ts",
"tests/semuta-homeworld-battle-winner.test.ts"
]
}
],