vpp-ai-platform/packages/domain/scripts/make-fixtures.ts
stewart hu f681e134cc M1: domain schemas, contracts pipeline, ledger and snapshot services
- packages/domain: zod schemas for the 07-scenario object set with a
  schemaRegistry driving schema export, fixture generation, and Python
  module naming; decimal-string/sha256/curve primitives enforce the
  docs/11 §3.3 representation rules at the type layer
- contracts/: 12 JSON Schemas (draft 2020-12), 12 golden fixtures, 3
  invalid fixtures crafted to fail on both sides (float money, bad
  digest, missing concurrency version)
- skills-py: generated pydantic models (committed, never hand-edited),
  regeneration script, mirror pytest using JSON-mode validation —
  dual-side contract tests agree on all 15 fixtures
- packages/services: content-addressed immutable snapshot store
  (memory + fs, rejects floats), LedgerService v1 with optimistic
  concurrency and the P7 constraint cascade (monthly position bounds
  day-ahead bids; no-anchor bids rejected), quality-gate stub
- cascade band shape is an OPEN-QUESTION A5 placeholder (pro-rata
  daily share ±band) — see ledger.ts checkCascade

All green: tsc typecheck, 41 TS tests, 15 Python tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019u5SLNweVio6ozJX7yfxQr

🔮 View transcript: https://logs.lojong.info/s/e8u90k3t33w590r7b5y7yzqh
2026-09-01 21:50:22 -04:00

245 lines
6.9 KiB
TypeScript

/**
* 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,
},
},
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' },
},
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')