Normalize actionable dashboard payload vocabulary
All checks were successful
deploy / deploy (push) Successful in 34s
All checks were successful
deploy / deploy (push) Successful in 34s
Proof: Operator-facing dashboard payloads no longer expose the forbidden actionable decision vocabulary while preserving lifecycle truth and executor-versus-strategy separation. Assumptions: Existing stored decision records may still use actionable internally, so the dashboard layer must normalize them before exposure. Still fake: Downstream venue completion evidence is still unavailable for submission-only rows, so submitted remains a non-terminal evidence state.
This commit is contained in:
parent
e35bb9ab8f
commit
715a0aec50
2 changed files with 82 additions and 4 deletions
|
|
@ -131,7 +131,7 @@ const CONTROL_DEFINITIONS = [
|
|||
method: 'POST',
|
||||
path: '/arm',
|
||||
label: 'Arm Executor',
|
||||
description: 'Persistently arm trade-executor so actionable decisions can submit.',
|
||||
description: 'Persistently arm trade-executor so strategy-approved decisions can submit.',
|
||||
page: 'system',
|
||||
risk_class: 'safe',
|
||||
},
|
||||
|
|
@ -736,6 +736,7 @@ const HUMAN_REASON_TEXT = {
|
|||
quote_response_ok: 'Quote response accepted by the relay.',
|
||||
reason_unknown: 'Reason not recorded.',
|
||||
stale_reference_price: 'Reference price is stale.',
|
||||
strategy_approved: 'Strategy approved the quote.',
|
||||
strategy_disarmed: 'Strategy is disarmed.',
|
||||
submission_failed: 'Submission failed.',
|
||||
unsupported_pair: 'Unsupported pair.',
|
||||
|
|
@ -910,7 +911,7 @@ function finalizeLifecycleRow(row) {
|
|||
} else if (decision?.decision) {
|
||||
lifecycle_state = 'evaluated';
|
||||
lifecycle_label = 'Approved by strategy';
|
||||
reason_code = normalizeLifecycleToken(decision?.decision_reason || 'actionable');
|
||||
reason_code = normalizeLifecycleToken(decision?.decision_reason || 'strategy_approved');
|
||||
reason_text = 'Strategy approved the quote, but no durable execute command is recorded yet.';
|
||||
}
|
||||
|
||||
|
|
@ -1206,6 +1207,7 @@ function normalizeTradeForUi({ config, trade }) {
|
|||
|
||||
return {
|
||||
...trade,
|
||||
decision_reason: normalizeDecisionReason(trade.decision_reason),
|
||||
asset_in_symbol: assetIn?.symbol || trade.asset_in,
|
||||
asset_out_symbol: assetOut?.symbol || trade.asset_out,
|
||||
amount_in_display: formatUnits(trade.amount_in || '0', assetIn?.decimals || 0),
|
||||
|
|
@ -1341,8 +1343,8 @@ function normalizeDecision(decision) {
|
|||
pair: decision.pair || null,
|
||||
direction: decision.direction || null,
|
||||
request_kind: decision.request_kind || null,
|
||||
decision: decision.decision || null,
|
||||
decision_reason: decision.decision_reason || null,
|
||||
decision: normalizeDecisionVerdict(decision.decision),
|
||||
decision_reason: normalizeDecisionReason(decision.decision_reason),
|
||||
gross_edge_pct: decision.gross_edge_pct || null,
|
||||
threshold_pct: decision.threshold_pct || null,
|
||||
max_notional_eure: decision.max_notional_eure || null,
|
||||
|
|
@ -1352,6 +1354,16 @@ function normalizeDecision(decision) {
|
|||
};
|
||||
}
|
||||
|
||||
function normalizeDecisionVerdict(value) {
|
||||
if (value === 'actionable') return 'approved';
|
||||
return value || null;
|
||||
}
|
||||
|
||||
function normalizeDecisionReason(value) {
|
||||
if (value === 'actionable') return 'strategy_approved';
|
||||
return value || null;
|
||||
}
|
||||
|
||||
function normalizeAlertList(alerts) {
|
||||
return (alerts || []).map(normalizeAlert).sort((left, right) => sortTimestamps(
|
||||
right.raised_at || right.first_raised_at || right.cleared_at,
|
||||
|
|
|
|||
|
|
@ -299,6 +299,7 @@ test('strategy approval no longer renders the forbidden actionable label', () =>
|
|||
assert.equal(rows[0].lifecycle_state, 'evaluated');
|
||||
assert.equal(rows[0].lifecycle_label, 'Approved by strategy');
|
||||
assert.notEqual(rows[0].lifecycle_label, 'Actionable');
|
||||
assert.equal(rows[0].reason_code, 'strategy_approved');
|
||||
});
|
||||
|
||||
test('bootstrap aggregation keeps Funds as default and carries live control state', () => {
|
||||
|
|
@ -496,6 +497,71 @@ test('bootstrap aggregation keeps Funds as default and carries live control stat
|
|||
assert.equal(bootstrap.strategy.strategy_state.recent_lifecycle_rows[0].reason_code, 'strategy_disarmed');
|
||||
});
|
||||
|
||||
test('bootstrap normalizes actionable decision vocabulary before exposing it to the dashboard', () => {
|
||||
const config = buildConfig();
|
||||
const bootstrap = buildDashboardBootstrap({
|
||||
config,
|
||||
auth: { authenticated: true, subject: 'local-operator', mode: 'stub', roles: ['operator'] },
|
||||
portfolioMetric: null,
|
||||
inventorySnapshot: null,
|
||||
marketPrice: null,
|
||||
recentQuotes: [],
|
||||
submissionPage: {
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
total: 1,
|
||||
total_pages: 1,
|
||||
items: [{
|
||||
quote_id: 'quote-1',
|
||||
pair: config.activePair,
|
||||
observed_at: '2026-04-04T09:10:03.000Z',
|
||||
status: 'submitted',
|
||||
result_code: 'quote_response_ok',
|
||||
decision_reason: 'actionable',
|
||||
}],
|
||||
},
|
||||
submissionSummary: {
|
||||
total: 1,
|
||||
last_submission_at: '2026-04-04T09:10:03.000Z',
|
||||
},
|
||||
fundingObservations: [],
|
||||
recentDepositStatuses: [],
|
||||
recentTradeDecisions: [{
|
||||
observed_at: '2026-04-04T09:10:00.000Z',
|
||||
payload: {
|
||||
decision_id: 'decision-1',
|
||||
quote_id: 'quote-1',
|
||||
pair: config.activePair,
|
||||
decision: 'actionable',
|
||||
decision_reason: 'actionable',
|
||||
},
|
||||
}],
|
||||
recentExecuteTradeCommands: [{
|
||||
observed_at: '2026-04-04T09:10:01.000Z',
|
||||
command_id: 'cmd-1',
|
||||
decision_id: 'decision-1',
|
||||
quote_id: 'quote-1',
|
||||
pair: config.activePair,
|
||||
}],
|
||||
recentExecutionResults: [{
|
||||
command_id: 'cmd-1',
|
||||
decision_id: 'decision-1',
|
||||
quote_id: 'quote-1',
|
||||
pair: config.activePair,
|
||||
result_at: '2026-04-04T09:10:03.000Z',
|
||||
status: 'submitted',
|
||||
result_code: 'quote_response_ok',
|
||||
}],
|
||||
recentAlertTransitions: [],
|
||||
serviceSnapshots: [],
|
||||
});
|
||||
|
||||
assert.equal(bootstrap.funds.submission_ledger.items[0].decision_reason, 'strategy_approved');
|
||||
assert.equal(bootstrap.strategy.strategy_state.recent_decisions[0].decision, 'approved');
|
||||
assert.equal(bootstrap.strategy.strategy_state.recent_lifecycle_rows[0].reason_code, 'quote_response_ok');
|
||||
assert.doesNotMatch(JSON.stringify(bootstrap), /Actionable/);
|
||||
});
|
||||
|
||||
test('system service health uses sentinel-derived severity so stale ingest is never shown healthy', () => {
|
||||
const config = buildConfig();
|
||||
const bootstrap = buildDashboardBootstrap({
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue