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',
|
method: 'POST',
|
||||||
path: '/arm',
|
path: '/arm',
|
||||||
label: 'Arm Executor',
|
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',
|
page: 'system',
|
||||||
risk_class: 'safe',
|
risk_class: 'safe',
|
||||||
},
|
},
|
||||||
|
|
@ -736,6 +736,7 @@ const HUMAN_REASON_TEXT = {
|
||||||
quote_response_ok: 'Quote response accepted by the relay.',
|
quote_response_ok: 'Quote response accepted by the relay.',
|
||||||
reason_unknown: 'Reason not recorded.',
|
reason_unknown: 'Reason not recorded.',
|
||||||
stale_reference_price: 'Reference price is stale.',
|
stale_reference_price: 'Reference price is stale.',
|
||||||
|
strategy_approved: 'Strategy approved the quote.',
|
||||||
strategy_disarmed: 'Strategy is disarmed.',
|
strategy_disarmed: 'Strategy is disarmed.',
|
||||||
submission_failed: 'Submission failed.',
|
submission_failed: 'Submission failed.',
|
||||||
unsupported_pair: 'Unsupported pair.',
|
unsupported_pair: 'Unsupported pair.',
|
||||||
|
|
@ -910,7 +911,7 @@ function finalizeLifecycleRow(row) {
|
||||||
} else if (decision?.decision) {
|
} else if (decision?.decision) {
|
||||||
lifecycle_state = 'evaluated';
|
lifecycle_state = 'evaluated';
|
||||||
lifecycle_label = 'Approved by strategy';
|
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.';
|
reason_text = 'Strategy approved the quote, but no durable execute command is recorded yet.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1206,6 +1207,7 @@ function normalizeTradeForUi({ config, trade }) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...trade,
|
...trade,
|
||||||
|
decision_reason: normalizeDecisionReason(trade.decision_reason),
|
||||||
asset_in_symbol: assetIn?.symbol || trade.asset_in,
|
asset_in_symbol: assetIn?.symbol || trade.asset_in,
|
||||||
asset_out_symbol: assetOut?.symbol || trade.asset_out,
|
asset_out_symbol: assetOut?.symbol || trade.asset_out,
|
||||||
amount_in_display: formatUnits(trade.amount_in || '0', assetIn?.decimals || 0),
|
amount_in_display: formatUnits(trade.amount_in || '0', assetIn?.decimals || 0),
|
||||||
|
|
@ -1341,8 +1343,8 @@ function normalizeDecision(decision) {
|
||||||
pair: decision.pair || null,
|
pair: decision.pair || null,
|
||||||
direction: decision.direction || null,
|
direction: decision.direction || null,
|
||||||
request_kind: decision.request_kind || null,
|
request_kind: decision.request_kind || null,
|
||||||
decision: decision.decision || null,
|
decision: normalizeDecisionVerdict(decision.decision),
|
||||||
decision_reason: decision.decision_reason || null,
|
decision_reason: normalizeDecisionReason(decision.decision_reason),
|
||||||
gross_edge_pct: decision.gross_edge_pct || null,
|
gross_edge_pct: decision.gross_edge_pct || null,
|
||||||
threshold_pct: decision.threshold_pct || null,
|
threshold_pct: decision.threshold_pct || null,
|
||||||
max_notional_eure: decision.max_notional_eure || 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) {
|
function normalizeAlertList(alerts) {
|
||||||
return (alerts || []).map(normalizeAlert).sort((left, right) => sortTimestamps(
|
return (alerts || []).map(normalizeAlert).sort((left, right) => sortTimestamps(
|
||||||
right.raised_at || right.first_raised_at || right.cleared_at,
|
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_state, 'evaluated');
|
||||||
assert.equal(rows[0].lifecycle_label, 'Approved by strategy');
|
assert.equal(rows[0].lifecycle_label, 'Approved by strategy');
|
||||||
assert.notEqual(rows[0].lifecycle_label, 'Actionable');
|
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', () => {
|
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');
|
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', () => {
|
test('system service health uses sentinel-derived severity so stale ingest is never shown healthy', () => {
|
||||||
const config = buildConfig();
|
const config = buildConfig();
|
||||||
const bootstrap = buildDashboardBootstrap({
|
const bootstrap = buildDashboardBootstrap({
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue