feat: support extra health state mechanics
Validate and deploy production / validate (push) Successful in 21s
Validate and deploy staging / validate (push) Successful in 23s
Validate and deploy production / deploy (push) Successful in 44s
Validate and deploy staging / deploy (push) Successful in 30s

This commit is contained in:
jb
2026-07-31 23:55:49 -04:00
parent 9ac2fe47cf
commit 1998e87309
16 changed files with 575 additions and 47 deletions
@@ -12,6 +12,7 @@
parseConditionalMechanicDraft,
readCurrentHealthPercentBand,
readEventFieldComparison,
registeredEventFields,
readStatePoolThreshold,
statePoolThresholdPredicate,
type ConditionalMechanicClauseDraft,
@@ -171,6 +172,41 @@
: options;
}
function eventPredicateFields(clause: ConditionalMechanicClauseDraft) {
return registeredEventFields(clause.event);
}
function selectedEventPredicateField(clause: ConditionalMechanicClauseDraft) {
const comparison = readEventFieldComparison(clause.if);
return eventPredicateFields(clause).find((field) => field.path === comparison?.path);
}
function eventPredicateOperatorOptions(clause: ConditionalMechanicClauseDraft) {
return selectedEventPredicateField(clause)?.type === 'number'
? predicateOperatorOptions
: predicateOperatorOptions.filter((option) => option.value === 'eq' || option.value === 'ne');
}
function setClauseEvent(clauseIndex: number, event: string) {
const nextEvent = event as ConditionalMechanicClauseDraft['event'];
const clause = rule.clauses[clauseIndex];
const fields = registeredEventFields(nextEvent);
const comparison = readEventFieldComparison(clause.if);
if (!comparison || !fields.length) {
updateClause(clauseIndex, { event: nextEvent });
return;
}
const field = fields[0];
updateClause(clauseIndex, {
event: nextEvent,
if: eventFieldComparisonPredicate({
path: field.path,
operator: 'eq',
value: field.type === 'boolean' ? true : field.type === 'number' ? 0 : ''
})
});
}
function setConditionModel(clauseIndex: number, model: string) {
if (model === 'always') updateClause(clauseIndex, { if: { kind: 'true' } });
if (model === 'current_health_band') {
@@ -179,11 +215,12 @@
});
}
if (model === 'event_field') {
const field = eventPredicateFields(rule.clauses[clauseIndex])[0];
updateClause(clauseIndex, {
if: eventFieldComparisonPredicate({
path: 'values.state',
path: field?.path ?? 'values.state',
operator: 'eq',
value: 'moving'
value: field?.type === 'boolean' ? true : field?.type === 'number' ? 0 : 'moving'
})
});
}
@@ -243,6 +280,18 @@
updateEventFieldComparison(clauseIndex, { value });
}
function setRegisteredEventField(clauseIndex: number, path: string) {
const clause = rule.clauses[clauseIndex];
const field = eventPredicateFields(clause).find((item) => item.path === path);
if (!field) return;
const current = readEventFieldComparison(clause.if);
updateEventFieldComparison(clauseIndex, {
path,
operator: field.type === 'number' ? current?.operator ?? 'eq' : 'eq',
value: field.type === 'boolean' ? true : field.type === 'number' ? 0 : ''
});
}
function updateStatePoolThreshold(
clauseIndex: number,
patch: Partial<StatePoolThresholdComparison>
@@ -839,6 +888,8 @@
{#each rule.clauses as clause, clauseIndex}
{@const conditionBand = readCurrentHealthPercentBand(clause.if)}
{@const eventFieldCondition = readEventFieldComparison(clause.if)}
{@const registeredPredicateField = selectedEventPredicateField(clause)}
{@const registeredPredicateFields = eventPredicateFields(clause)}
{@const statePoolCondition = readStatePoolThreshold(clause.if)}
<section class="branch">
<header>
@@ -846,7 +897,7 @@
<button type="button" class="remove" onclick={() => setRule({...rule,clauses:rule.clauses.filter((_,index) => index !== clauseIndex)})}>REMOVE BRANCH</button>
</header>
<div class="branch-grid">
<div class="field"><span>DIRECT EVENT</span><SearchableAssignment items={eventItems} mode="single" compact subjectLabel="EVENT" bind:value={clause.event}/></div>
<div class="field"><span>DIRECT EVENT</span><SearchableAssignment items={eventItems} mode="single" compact subjectLabel="EVENT" value={clause.event} onvaluechange={(value) => setClauseEvent(clauseIndex,value)}/></div>
<div class="branch-readout"><span>MATCH POLICY</span><strong>{reducerItems.find((item) => item.id === clause.reducer.kind)?.name ?? clause.reducer.kind}</strong></div>
<label class="field">COOLDOWN · SECONDS<input type="number" min="0.01" step="any" value={clause.cooldownSeconds ?? ''} oninput={(event) => updateClause(clauseIndex,{cooldownSeconds:event.currentTarget.value ? Number(event.currentTarget.value) : undefined})}/></label>
</div>
@@ -865,22 +916,34 @@
</div>
{:else if eventFieldCondition}
<div class="predicate-fields event-predicate-fields">
<label class="field">EVENT FIELD PATH<input value={eventFieldCondition.path} placeholder="values.state" oninput={(event) => updateEventFieldComparison(clauseIndex,{path:event.currentTarget.value})}/></label>
{#if registeredPredicateFields.length && registeredPredicateField}
<SegmentedChoice
legend="EVENT FIELD"
name={`${idPrefix}-event-field-${clauseIndex}`}
options={registeredPredicateFields.map((field) => ({value:field.path,label:field.label.toUpperCase()}))}
value={registeredPredicateField.path}
onchange={(values) => setRegisteredEventField(clauseIndex,values[0])}
/>
{:else}
<label class="field">EVENT FIELD PATH<input value={eventFieldCondition.path} placeholder="values.state" oninput={(event) => updateEventFieldComparison(clauseIndex,{path:event.currentTarget.value})}/></label>
{/if}
<SegmentedChoice
legend="COMPARISON"
name={`${idPrefix}-event-operator-${clauseIndex}`}
options={predicateOperatorOptions}
options={eventPredicateOperatorOptions(clause)}
value={eventFieldCondition.operator}
columns={6}
columns={eventPredicateOperatorOptions(clause).length}
onchange={(values) => updateEventFieldComparison(clauseIndex,{operator:values[0] as PredicateComparisonOperator})}
/>
<SegmentedChoice
legend="VALUE TYPE"
name={`${idPrefix}-event-value-type-${clauseIndex}`}
options={predicateScalarTypeOptions}
value={predicateScalarType(eventFieldCondition.value)}
onchange={(values) => setEventFieldScalarType(clauseIndex,values[0])}
/>
{#if !registeredPredicateField}
<SegmentedChoice
legend="VALUE TYPE"
name={`${idPrefix}-event-value-type-${clauseIndex}`}
options={predicateScalarTypeOptions}
value={predicateScalarType(eventFieldCondition.value)}
onchange={(values) => setEventFieldScalarType(clauseIndex,values[0])}
/>
{/if}
{#if typeof eventFieldCondition.value === 'number'}
<label class="field">COMPARE VALUE<input type="number" step="any" value={eventFieldCondition.value} oninput={(event) => updateEventFieldComparison(clauseIndex,{value:Number(event.currentTarget.value)})}/></label>
{:else if typeof eventFieldCondition.value === 'boolean'}