vpp-ai-platform/packages/domain/scripts/make-fixtures.ts
Thomas Bayes 1cc21e0dc6
Some checks are pending
ci / typescript (push) Waiting to run
ci / python (push) Waiting to run
ci / evals (push) Blocked by required conditions
M3: Mastra runtime, safety chain, two agents, Case Desk v1
- packages/domain: safety-chain objects (ValidationResult, SimulationResult,
  EnvelopeMatch, StaleDenial, ExecutionReceipt, LineageRef/BidProposalDraft,
  RouterDecision, BidExportFile) + fixtures on both sides.
- packages/services: proposal digest; PolicyEngine + hubei-spot-bidding pack
  (digest-valid, bid-format, price-limits, quantity-non-negative,
  ledger-consistency, lineage-integrity, originator-permission — each with
  pass/fail tests); EnvelopeService; AuthorityService (fresh check, permits,
  revoke, gateway validate); FileExportGateway (idempotent receipts);
  Memory/File EventBus; LineageRecorder + P2 assembler; RevenueScenario
  simulator; CaseDeskService; FsRepository; skill HTTP client moved here.
- packages/runtime: createRuntime (LibSQL storage, per-runtime workflow
  factories), proposal-lifecycle (rule check → simulation → envelope gate
  with suspend/resume → fresh check + permit → release), day-ahead-situation,
  day-ahead-bid, TriggerService (scheduled/event/manual), LlmPort
  (Mastra/Scripted/Null), Case Desk HTTP API, dev entry point.
- Tests: all eight docs/01 invariants, docs/07 06:00→08:30 end to end with
  LLM down, restart survival of a suspended approval, permit expiry and
  revocation, replay of a released proposal, trigger scheduling. 141 TS +
  60 Python tests.
- Known gaps: ledger not yet persisted (replayed on restart); STALE ends the
  run instead of looping to rule check; synthetic data stands in for
  historical replay.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UoYoGYzHkFyv3ALenkRPhA
2026-09-02 06:55:55 -04:00

468 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Generates golden fixtures into contracts/fixtures/<name>/*.json and invalid
* fixtures into contracts/fixtures-invalid/<name>/*.json. Run once and commit;
* fixtures are reviewed via diff. Invalid cases are crafted to fail on BOTH the
* zod and pydantic sides (dual-side contract test, docs/11 §3.2).
*/
import { mkdirSync, writeFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { schemaRegistry } from '../src/index.js'
const root = fileURLToPath(new URL('../../../contracts/', import.meta.url))
const REF_A = 'a'.repeat(64)
const REF_B = 'b'.repeat(64)
const REF_C = 'c'.repeat(64)
const DATE = '2026-03-15'
const T0 = '2026-03-14T06:00:00Z'
const T1 = '2026-03-14T08:30:00Z'
const T2 = '2026-03-15T23:59:59Z'
const curve = (v: string, date = DATE) => ({
interval_minutes: 15,
date,
values: Array.from({ length: 96 }, () => v),
})
const valid: Record<string, Record<string, unknown>> = {
proposal: {
'bid-basic': {
id: 'prop-001',
type: 'BID',
timescale: 'DAY_AHEAD',
originator: { agent: 'trading-agent', trigger: 'SCHEDULED', task_id: 'task-001' },
payload: {
kind: 'BID',
market_date: DATE,
prices_yuan_per_mwh: curve('425.50'),
quantities_mwh: curve('12.5'),
expected_revenue_yuan: '510600.00',
},
lineage: {
tool_calls: [
{
tool_call_id: 'tc-001',
tool: 'bid-optimization-milp',
version: '1.0.0',
inputs_ref: REF_A,
outputs_ref: REF_B,
},
],
data_refs: [REF_C],
ledger_version: 42,
policy_pack_version: '2026.03',
},
envelope_ref: 'env-bid-001',
digest: REF_B,
status: 'DRAFT',
created_at: T1,
},
},
approval: {
approve: {
id: 'appr-001',
proposal_digest: REF_B,
decision: 'APPROVE',
approver: { id: 'user-trader-01', role: 'senior-trader' },
scope: { effect_type: 'BID', limits: { max_energy_mwh: '1500.0' } },
validity: { from: T1, to: T2 },
evidence_versions: {
policy_pack_version: '2026.03',
ledger_version: 42,
data_snapshot_refs: [REF_C],
},
comment: 'within monthly position band',
decided_at: T1,
},
},
execution_permit: {
active: {
id: 'permit-001',
proposal_digest: REF_B,
issued_at: T1,
expires_at: T2,
effect_limits: { max_energy_mwh: '1500.0' },
revoked_at: null,
issuer: 'authority-service',
},
},
envelope: {
'bid-envelope': {
id: 'env-bid-001',
scope: { proposal_type: 'BID', timescales: ['DAY_AHEAD'], resource_set: 'pool-hubei-01' },
bounds: { price_deviation_pct: '5.0', max_energy_mwh: '1500.0' },
validity: { from: T0, to: T2 },
approval: { level: 'L2', approved_by: ['user-ops-lead', 'user-risk-officer'] },
escalation: { max_consecutive_deviations: 3, deviation_threshold_pct: '10.0' },
status: 'ACTIVE',
},
},
flexibility_envelope: {
'da-aggregation-unit': {
id: 'flex-001',
provider: { unit_id: 'agg-unit-wuhan-01', level: 'AGGREGATION_UNIT' },
market_date: DATE,
up_mw: curve('8.5'),
down_mw: curve('6.0'),
valid_until: T2,
schema_version: '1.0.0',
signature: null,
},
},
position_update: {
'award-da': {
id: 'pos-001',
timescale: 'DAY_AHEAD',
period: DATE,
kind: 'AWARD',
energy_mwh: '1180.0',
curve: curve('12.3'),
source_ref: REF_B,
expected_version: 42,
},
},
ledger_view: {
basic: {
version: 43,
entries: [
{
id: 'pos-000',
timescale: 'MONTHLY',
period: '2026-03',
kind: 'CONTRACT',
energy_mwh: '36000.0',
curve: null,
source_ref: 'contract-2026-03-001',
recorded_at: T0,
},
],
},
},
forecast_bundle: {
'load-da': {
id: 'fc-001',
kind: 'LOAD',
market_date: DATE,
unit: 'mw',
quantiles: { p10: curve('40.1'), p50: curve('45.7'), p90: curve('52.3') },
model: { name: 'load-forecast', version: '1.2.0' },
features_snapshot_ref: REF_A,
generated_at: T0,
},
},
forecast_request: {
'price-da': {
kind: 'PRICE',
market_date: DATE,
unit: 'yuan_per_mwh',
history: [curve('398.20', '2026-03-13'), curve('410.75', '2026-03-14')],
exogenous: {},
features_snapshot_ref: REF_A,
},
},
bid_optimization_request: {
'da-basic': {
market_date: DATE,
price_forecast: {
id: 'fc-price-001',
kind: 'PRICE',
market_date: DATE,
unit: 'yuan_per_mwh',
quantiles: { p10: curve('360.00'), p50: curve('425.50'), p90: curve('490.00') },
model: { name: 'price-forecast', version: '1.0.0' },
features_snapshot_ref: REF_A,
generated_at: T0,
},
adjustable_capacity_mw: curve('60.0'),
position_bounds: {
ledger_version: 42,
daily_energy_min_mwh: '1140.0',
daily_energy_max_mwh: '1260.0',
},
risk: {
risk_aversion: '0.3',
commitment_buffer_k: '0.9',
min_block_mwh: '1.0',
marginal_cost_yuan_per_mwh: '0',
},
},
},
bid_optimization_result: {
'da-basic': {
market_date: DATE,
prices_yuan_per_mwh: curve('0.00'),
quantities_mwh: curve('12.5'),
daily_energy_mwh: '1200.0',
expected_revenue_yuan: '510600.00',
revenue_distribution_yuan: { p10: '432000.00', p50: '510600.00', p90: '588000.00' },
position_bounds: {
ledger_version: 42,
daily_energy_min_mwh: '1140.0',
daily_energy_max_mwh: '1260.0',
},
solver: {
name: 'highs',
version: '1.7.0',
status: 'OPTIMAL',
objective_value: '487020.00',
wall_time_ms: 12,
},
binding_constraints: ['daily_energy_max'],
skill_version: '1.0.0',
},
},
report_request: {
'da-bid-summary': {
kind: 'DAY_AHEAD_BID_SUMMARY',
market_date: DATE,
sources: [
{
tool_call_id: 'tc-001',
tool: 'bid-optimization-milp',
version: '1.0.0',
output: { expected_revenue_yuan: '510600.00', daily_energy_mwh: '1200.0' },
},
],
},
},
skill_report: {
'da-bid-summary': {
id: 'rep-001',
kind: 'DAY_AHEAD_BID_SUMMARY',
market_date: DATE,
sections: [
{
title: 'Bid summary',
metrics: [
{
name: 'expected_revenue',
value: '510600.00',
unit: 'yuan',
ref: { tool_call_id: 'tc-001', path: 'expected_revenue_yuan' },
},
],
notes: ['all figures reference solver output tc-001'],
},
],
skill_version: '1.0.0',
generated_at: T1,
},
},
validation_result: {
pass: {
proposal_digest: REF_B,
policy_pack: { id: 'hubei-spot-bidding', version: '2026.03' },
ok: true,
violations: [],
rules_evaluated: ['bid-format', 'price-limits', 'ledger-consistency', 'lineage-integrity'],
checked_at: T1,
},
'reject-price': {
proposal_digest: REF_B,
policy_pack: { id: 'hubei-spot-bidding', version: '2026.03' },
ok: false,
violations: [{ rule_id: 'price-limits', severity: 'REJECT', message: 'price 1500.00 above cap' }],
rules_evaluated: ['bid-format', 'price-limits'],
checked_at: T1,
},
},
simulation_result: {
'revenue-ok': {
proposal_digest: REF_B,
kind: 'REVENUE_SCENARIOS',
alert: false,
alerts: [],
metrics: { revenue_p05_yuan: '410200.00', revenue_p50_yuan: '510600.00', worst_loss_yuan: '0.00' },
scenario_count: 1000,
simulator: { name: 'revenue-scenarios', version: '1.0.0', seed: 20260315 },
simulated_at: T1,
},
},
envelope_match: {
within: {
proposal_digest: REF_B,
within: true,
envelope_id: 'env-bid-001',
required_level: 'L1',
reasons: [],
checked_at: T1,
},
outside: {
proposal_digest: REF_B,
within: false,
envelope_id: null,
required_level: 'L2',
reasons: ['no ACTIVE envelope covers BID/DAY_AHEAD for pool-hubei-01'],
checked_at: T1,
},
},
stale_denial: {
'ledger-moved': {
proposal_digest: REF_B,
reasons: ['ledger version 42 at approval, now 43'],
checked_at: T2,
},
},
execution_receipt: {
'file-export': {
receipt_id: 'rcpt-001',
proposal_digest: REF_B,
permit_id: 'permit-001',
channel: 'FILE_EXPORT',
idempotency_key: `${REF_B}:permit-001`,
artifact_ref: 'exports/bid-2026-03-15-prop-001.json',
accepted_at: T1,
},
},
bid_proposal_draft: {
basic: {
market_date: DATE,
prices_ref: { tool_call_id: 'tc-001', path: 'prices_yuan_per_mwh' },
quantities_ref: { tool_call_id: 'tc-001', path: 'quantities_mwh' },
expected_revenue_ref: { tool_call_id: 'tc-001', path: 'expected_revenue_yuan' },
rationale: 'Evening peak priced above P50; allocate bounded energy to 18:00–20:00.',
},
},
router_decision: {
'day-ahead-bid': {
workflow_id: 'day-ahead-bid',
params: { market_date: DATE },
confidence: 'HIGH',
},
},
bid_export_file: {
basic: {
format_version: '1.0.0',
proposal_id: 'prop-001',
proposal_digest: REF_B,
permit_id: 'permit-001',
proposal_type: 'BID',
market_date: DATE,
prices_yuan_per_mwh: curve('0.00'),
quantities_mwh: curve('12.5'),
exported_at: T1,
},
},
situation_report: {
'normal-day': {
id: 'sit-001',
market_date: DATE,
risk_level: 'LOW',
findings: [
{ kind: 'TREND', summary: 'mild temperatures, load near seasonal norm', refs: [REF_A] },
],
forecast_refs: ['fc-001'],
generated_at: T0,
},
},
resource_profile: {
storage: {
resource_id: 'res-storage-01',
name: 'Wuhan industrial park storage #1',
type: 'STORAGE',
rated_power_mw: '10.0',
certified_adjustable_mw: '8.0',
confidence: '0.92',
reliability_score: '0.88',
constraints: { min_duration_min: 60, recovery_rate_mw_per_min: '0.5' },
evidence_refs: [REF_C],
updated_at: T0,
},
},
decision_case: {
'da-bid': {
id: 'case-001',
kind: 'DAY_AHEAD_BID',
objective: 'Complete day-ahead bid for 2026-03-15',
owner: 'user-trader-01',
deadline: '2026-03-14T10:00:00Z',
status: 'AWAITING_APPROVAL',
refs: {
evidence: ['sit-001', 'fc-001'],
proposals: ['prop-001'],
approvals: [],
permits: [],
scenarios: [],
outcome: null,
},
opened_at: T0,
closed_at: null,
},
},
event_envelope: {
'situation-published': {
event_id: 'evt-001',
event_type: 'SituationReportPublished',
schema_version: '1.0.0',
occurred_at: T0,
causation_id: 'task-001',
correlation_id: 'case-001',
payload: { situation_report_id: 'sit-001' },
},
},
}
// Each invalid case breaks exactly one rule and must fail on both sides.
const invalid: Record<string, Record<string, unknown>> = {
proposal: {
// money as JSON float — the forbidden representation (docs/11 §3.3)
'bid-float-money': structuredClone(valid['proposal']!['bid-basic']!),
},
approval: {
// digest not sha256 hex
'bad-digest': { ...(valid['approval']!['approve'] as object), proposal_digest: 'not-a-digest' },
},
skill_report: {
// a metric without a lineage reference — the one thing a report may never contain (I1)
'metric-without-ref': (() => {
const o = structuredClone(valid['skill_report']!['da-bid-summary']) as any
delete o.sections[0].metrics[0].ref
return o
})(),
},
bid_optimization_result: {
// solver status outside the enum
'bad-solver-status': (() => {
const o = structuredClone(valid['bid_optimization_result']!['da-basic']) as any
o.solver.status = 'SOLVED'
return o
})(),
},
bid_proposal_draft: {
// a literal number where only a lineage reference is allowed (I1/P2)
'literal-number': {
market_date: DATE,
prices_ref: { tool_call_id: 'tc-001', path: 'prices_yuan_per_mwh' },
quantities_ref: { tool_call_id: 'tc-001', path: 'quantities_mwh' },
expected_revenue_ref: '510600.00',
rationale: 'x',
},
},
router_decision: {
// a workflow id that is not a registered template
'unknown-template': { workflow_id: 'transfer-funds', params: { market_date: DATE }, confidence: 'HIGH' },
},
position_update: {
// missing optimistic-concurrency field
'missing-version': (() => {
const o = structuredClone(valid['position_update']!['award-da']) as Record<string, unknown>
delete o['expected_version']
return o
})(),
},
}
;(invalid['proposal']!['bid-float-money'] as any).payload.expected_revenue_yuan = 510600.0
const write = (dir: string, sets: Record<string, Record<string, unknown>>) => {
for (const [name, cases] of Object.entries(sets)) {
mkdirSync(`${root}${dir}/${name}`, { recursive: true })
for (const [caseName, obj] of Object.entries(cases)) {
writeFileSync(`${root}${dir}/${name}/${caseName}.json`, JSON.stringify(obj, null, 2) + '\n')
}
}
}
write('fixtures', valid)
write('fixtures-invalid', invalid)
console.log('fixtures written')