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
This commit is contained in:
parent
80835138e9
commit
f681e134cc
28
contracts/fixtures-invalid/approval/bad-digest.json
Normal file
28
contracts/fixtures-invalid/approval/bad-digest.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"id": "appr-001",
|
||||||
|
"proposal_digest": "not-a-digest",
|
||||||
|
"decision": "APPROVE",
|
||||||
|
"approver": {
|
||||||
|
"id": "user-trader-01",
|
||||||
|
"role": "senior-trader"
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"effect_type": "BID",
|
||||||
|
"limits": {
|
||||||
|
"max_energy_mwh": "1500.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"validity": {
|
||||||
|
"from": "2026-03-14T08:30:00Z",
|
||||||
|
"to": "2026-03-15T23:59:59Z"
|
||||||
|
},
|
||||||
|
"evidence_versions": {
|
||||||
|
"policy_pack_version": "2026.03",
|
||||||
|
"ledger_version": 42,
|
||||||
|
"data_snapshot_refs": [
|
||||||
|
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"comment": "within monthly position band",
|
||||||
|
"decided_at": "2026-03-14T08:30:00Z"
|
||||||
|
}
|
||||||
110
contracts/fixtures-invalid/position_update/missing-version.json
Normal file
110
contracts/fixtures-invalid/position_update/missing-version.json
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
{
|
||||||
|
"id": "pos-001",
|
||||||
|
"timescale": "DAY_AHEAD",
|
||||||
|
"period": "2026-03-15",
|
||||||
|
"kind": "AWARD",
|
||||||
|
"energy_mwh": "1180.0",
|
||||||
|
"curve": {
|
||||||
|
"interval_minutes": 15,
|
||||||
|
"date": "2026-03-15",
|
||||||
|
"values": [
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"source_ref": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||||
|
}
|
||||||
239
contracts/fixtures-invalid/proposal/bid-float-money.json
Normal file
239
contracts/fixtures-invalid/proposal/bid-float-money.json
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
{
|
||||||
|
"id": "prop-001",
|
||||||
|
"type": "BID",
|
||||||
|
"timescale": "DAY_AHEAD",
|
||||||
|
"originator": {
|
||||||
|
"agent": "trading-agent",
|
||||||
|
"trigger": "SCHEDULED",
|
||||||
|
"task_id": "task-001"
|
||||||
|
},
|
||||||
|
"payload": {
|
||||||
|
"kind": "BID",
|
||||||
|
"market_date": "2026-03-15",
|
||||||
|
"prices_yuan_per_mwh": {
|
||||||
|
"interval_minutes": 15,
|
||||||
|
"date": "2026-03-15",
|
||||||
|
"values": [
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"quantities_mwh": {
|
||||||
|
"interval_minutes": 15,
|
||||||
|
"date": "2026-03-15",
|
||||||
|
"values": [
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"expected_revenue_yuan": 510600
|
||||||
|
},
|
||||||
|
"lineage": {
|
||||||
|
"tool_calls": [
|
||||||
|
{
|
||||||
|
"tool_call_id": "tc-001",
|
||||||
|
"tool": "bid-optimization-milp",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"inputs_ref": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
|
"outputs_ref": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"data_refs": [
|
||||||
|
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
|
||||||
|
],
|
||||||
|
"ledger_version": 42,
|
||||||
|
"policy_pack_version": "2026.03"
|
||||||
|
},
|
||||||
|
"envelope_ref": "env-bid-001",
|
||||||
|
"digest": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||||
|
"status": "DRAFT",
|
||||||
|
"created_at": "2026-03-14T08:30:00Z"
|
||||||
|
}
|
||||||
28
contracts/fixtures/approval/approve.json
Normal file
28
contracts/fixtures/approval/approve.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"id": "appr-001",
|
||||||
|
"proposal_digest": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||||
|
"decision": "APPROVE",
|
||||||
|
"approver": {
|
||||||
|
"id": "user-trader-01",
|
||||||
|
"role": "senior-trader"
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"effect_type": "BID",
|
||||||
|
"limits": {
|
||||||
|
"max_energy_mwh": "1500.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"validity": {
|
||||||
|
"from": "2026-03-14T08:30:00Z",
|
||||||
|
"to": "2026-03-15T23:59:59Z"
|
||||||
|
},
|
||||||
|
"evidence_versions": {
|
||||||
|
"policy_pack_version": "2026.03",
|
||||||
|
"ledger_version": 42,
|
||||||
|
"data_snapshot_refs": [
|
||||||
|
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"comment": "within monthly position band",
|
||||||
|
"decided_at": "2026-03-14T08:30:00Z"
|
||||||
|
}
|
||||||
23
contracts/fixtures/decision_case/da-bid.json
Normal file
23
contracts/fixtures/decision_case/da-bid.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"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": "2026-03-14T06:00:00Z",
|
||||||
|
"closed_at": null
|
||||||
|
}
|
||||||
30
contracts/fixtures/envelope/bid-envelope.json
Normal file
30
contracts/fixtures/envelope/bid-envelope.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"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": "2026-03-14T06:00:00Z",
|
||||||
|
"to": "2026-03-15T23:59:59Z"
|
||||||
|
},
|
||||||
|
"approval": {
|
||||||
|
"level": "L2",
|
||||||
|
"approved_by": [
|
||||||
|
"user-ops-lead",
|
||||||
|
"user-risk-officer"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"escalation": {
|
||||||
|
"max_consecutive_deviations": 3,
|
||||||
|
"deviation_threshold_pct": "10.0"
|
||||||
|
},
|
||||||
|
"status": "ACTIVE"
|
||||||
|
}
|
||||||
11
contracts/fixtures/event_envelope/situation-published.json
Normal file
11
contracts/fixtures/event_envelope/situation-published.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"event_id": "evt-001",
|
||||||
|
"event_type": "SituationReportPublished",
|
||||||
|
"schema_version": "1.0.0",
|
||||||
|
"occurred_at": "2026-03-14T06:00:00Z",
|
||||||
|
"causation_id": "task-001",
|
||||||
|
"correlation_id": "case-001",
|
||||||
|
"payload": {
|
||||||
|
"situation_report_id": "sit-001"
|
||||||
|
}
|
||||||
|
}
|
||||||
11
contracts/fixtures/execution_permit/active.json
Normal file
11
contracts/fixtures/execution_permit/active.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"id": "permit-001",
|
||||||
|
"proposal_digest": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||||
|
"issued_at": "2026-03-14T08:30:00Z",
|
||||||
|
"expires_at": "2026-03-15T23:59:59Z",
|
||||||
|
"effect_limits": {
|
||||||
|
"max_energy_mwh": "1500.0"
|
||||||
|
},
|
||||||
|
"revoked_at": null,
|
||||||
|
"issuer": "authority-service"
|
||||||
|
}
|
||||||
215
contracts/fixtures/flexibility_envelope/da-aggregation-unit.json
Normal file
215
contracts/fixtures/flexibility_envelope/da-aggregation-unit.json
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
{
|
||||||
|
"id": "flex-001",
|
||||||
|
"provider": {
|
||||||
|
"unit_id": "agg-unit-wuhan-01",
|
||||||
|
"level": "AGGREGATION_UNIT"
|
||||||
|
},
|
||||||
|
"market_date": "2026-03-15",
|
||||||
|
"up_mw": {
|
||||||
|
"interval_minutes": 15,
|
||||||
|
"date": "2026-03-15",
|
||||||
|
"values": [
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5",
|
||||||
|
"8.5"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"down_mw": {
|
||||||
|
"interval_minutes": 15,
|
||||||
|
"date": "2026-03-15",
|
||||||
|
"values": [
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0",
|
||||||
|
"6.0"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"valid_until": "2026-03-15T23:59:59Z",
|
||||||
|
"schema_version": "1.0.0",
|
||||||
|
"signature": null
|
||||||
|
}
|
||||||
320
contracts/fixtures/forecast_bundle/load-da.json
Normal file
320
contracts/fixtures/forecast_bundle/load-da.json
Normal file
@ -0,0 +1,320 @@
|
|||||||
|
{
|
||||||
|
"id": "fc-001",
|
||||||
|
"kind": "LOAD",
|
||||||
|
"market_date": "2026-03-15",
|
||||||
|
"unit": "mw",
|
||||||
|
"quantiles": {
|
||||||
|
"p10": {
|
||||||
|
"interval_minutes": 15,
|
||||||
|
"date": "2026-03-15",
|
||||||
|
"values": [
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1",
|
||||||
|
"40.1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"p50": {
|
||||||
|
"interval_minutes": 15,
|
||||||
|
"date": "2026-03-15",
|
||||||
|
"values": [
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7",
|
||||||
|
"45.7"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"p90": {
|
||||||
|
"interval_minutes": 15,
|
||||||
|
"date": "2026-03-15",
|
||||||
|
"values": [
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3",
|
||||||
|
"52.3"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"name": "load-forecast",
|
||||||
|
"version": "1.2.0"
|
||||||
|
},
|
||||||
|
"features_snapshot_ref": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
|
"generated_at": "2026-03-14T06:00:00Z"
|
||||||
|
}
|
||||||
15
contracts/fixtures/ledger_view/basic.json
Normal file
15
contracts/fixtures/ledger_view/basic.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"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": "2026-03-14T06:00:00Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
111
contracts/fixtures/position_update/award-da.json
Normal file
111
contracts/fixtures/position_update/award-da.json
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
{
|
||||||
|
"id": "pos-001",
|
||||||
|
"timescale": "DAY_AHEAD",
|
||||||
|
"period": "2026-03-15",
|
||||||
|
"kind": "AWARD",
|
||||||
|
"energy_mwh": "1180.0",
|
||||||
|
"curve": {
|
||||||
|
"interval_minutes": 15,
|
||||||
|
"date": "2026-03-15",
|
||||||
|
"values": [
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3",
|
||||||
|
"12.3"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"source_ref": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||||
|
"expected_version": 42
|
||||||
|
}
|
||||||
239
contracts/fixtures/proposal/bid-basic.json
Normal file
239
contracts/fixtures/proposal/bid-basic.json
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
{
|
||||||
|
"id": "prop-001",
|
||||||
|
"type": "BID",
|
||||||
|
"timescale": "DAY_AHEAD",
|
||||||
|
"originator": {
|
||||||
|
"agent": "trading-agent",
|
||||||
|
"trigger": "SCHEDULED",
|
||||||
|
"task_id": "task-001"
|
||||||
|
},
|
||||||
|
"payload": {
|
||||||
|
"kind": "BID",
|
||||||
|
"market_date": "2026-03-15",
|
||||||
|
"prices_yuan_per_mwh": {
|
||||||
|
"interval_minutes": 15,
|
||||||
|
"date": "2026-03-15",
|
||||||
|
"values": [
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50",
|
||||||
|
"425.50"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"quantities_mwh": {
|
||||||
|
"interval_minutes": 15,
|
||||||
|
"date": "2026-03-15",
|
||||||
|
"values": [
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"12.5",
|
||||||
|
"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": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
|
"outputs_ref": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"data_refs": [
|
||||||
|
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
|
||||||
|
],
|
||||||
|
"ledger_version": 42,
|
||||||
|
"policy_pack_version": "2026.03"
|
||||||
|
},
|
||||||
|
"envelope_ref": "env-bid-001",
|
||||||
|
"digest": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||||
|
"status": "DRAFT",
|
||||||
|
"created_at": "2026-03-14T08:30:00Z"
|
||||||
|
}
|
||||||
17
contracts/fixtures/resource_profile/storage.json
Normal file
17
contracts/fixtures/resource_profile/storage.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"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": [
|
||||||
|
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
|
||||||
|
],
|
||||||
|
"updated_at": "2026-03-14T06:00:00Z"
|
||||||
|
}
|
||||||
18
contracts/fixtures/situation_report/normal-day.json
Normal file
18
contracts/fixtures/situation_report/normal-day.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"id": "sit-001",
|
||||||
|
"market_date": "2026-03-15",
|
||||||
|
"risk_level": "LOW",
|
||||||
|
"findings": [
|
||||||
|
{
|
||||||
|
"kind": "TREND",
|
||||||
|
"summary": "mild temperatures, load near seasonal norm",
|
||||||
|
"refs": [
|
||||||
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"forecast_refs": [
|
||||||
|
"fc-001"
|
||||||
|
],
|
||||||
|
"generated_at": "2026-03-14T06:00:00Z"
|
||||||
|
}
|
||||||
136
contracts/schema/approval.json
Normal file
136
contracts/schema/approval.json
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"proposal_digest": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9a-f]{64}$"
|
||||||
|
},
|
||||||
|
"decision": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"APPROVE",
|
||||||
|
"REJECT"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"approver": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"role"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"effect_type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"BID",
|
||||||
|
"INVITATION",
|
||||||
|
"CONTROL_PLAN",
|
||||||
|
"DISPATCH_PLAN"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"limits": {
|
||||||
|
"type": "object",
|
||||||
|
"propertyNames": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?\\d+(\\.\\d+)?$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"effect_type",
|
||||||
|
"limits"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"validity": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"from": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||||
|
},
|
||||||
|
"to": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"from",
|
||||||
|
"to"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"evidence_versions": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"policy_pack_version": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"ledger_version": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 9007199254740991
|
||||||
|
},
|
||||||
|
"data_snapshot_refs": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9a-f]{64}$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"policy_pack_version",
|
||||||
|
"ledger_version",
|
||||||
|
"data_snapshot_refs"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"comment": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"decided_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"proposal_digest",
|
||||||
|
"decision",
|
||||||
|
"approver",
|
||||||
|
"scope",
|
||||||
|
"validity",
|
||||||
|
"evidence_versions",
|
||||||
|
"decided_at"
|
||||||
|
],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"$id": "https://vpp-ai-platform/contracts/approval.json",
|
||||||
|
"title": "Approval"
|
||||||
|
}
|
||||||
134
contracts/schema/decision_case.json
Normal file
134
contracts/schema/decision_case.json
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"DAY_AHEAD_BID",
|
||||||
|
"DR_EVENT",
|
||||||
|
"DEVIATION_RESOLUTION",
|
||||||
|
"REVIEW",
|
||||||
|
"ADHOC_ANALYSIS"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"objective": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"owner": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"deadline": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"OPEN",
|
||||||
|
"IN_PROGRESS",
|
||||||
|
"AWAITING_APPROVAL",
|
||||||
|
"CLOSED_DONE",
|
||||||
|
"CLOSED_ABORTED"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"refs": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"evidence": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"proposals": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"approvals": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"permits": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scenarios": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"outcome": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"evidence",
|
||||||
|
"proposals",
|
||||||
|
"approvals",
|
||||||
|
"permits",
|
||||||
|
"scenarios",
|
||||||
|
"outcome"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"opened_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||||
|
},
|
||||||
|
"closed_at": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"kind",
|
||||||
|
"objective",
|
||||||
|
"owner",
|
||||||
|
"deadline",
|
||||||
|
"status",
|
||||||
|
"refs",
|
||||||
|
"opened_at",
|
||||||
|
"closed_at"
|
||||||
|
],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"$id": "https://vpp-ai-platform/contracts/decision_case.json",
|
||||||
|
"title": "DecisionCase"
|
||||||
|
}
|
||||||
143
contracts/schema/envelope.json
Normal file
143
contracts/schema/envelope.json
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"proposal_type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"BID",
|
||||||
|
"INVITATION",
|
||||||
|
"CONTROL_PLAN",
|
||||||
|
"DISPATCH_PLAN"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"timescales": {
|
||||||
|
"minItems": 1,
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"ANNUAL",
|
||||||
|
"MONTHLY",
|
||||||
|
"DAY_AHEAD",
|
||||||
|
"INTRADAY",
|
||||||
|
"REALTIME"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resource_set": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"proposal_type",
|
||||||
|
"timescales",
|
||||||
|
"resource_set"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"bounds": {
|
||||||
|
"type": "object",
|
||||||
|
"propertyNames": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?\\d+(\\.\\d+)?$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"validity": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"from": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||||
|
},
|
||||||
|
"to": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"from",
|
||||||
|
"to"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"approval": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"level": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"L1",
|
||||||
|
"L2",
|
||||||
|
"L3"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"approved_by": {
|
||||||
|
"minItems": 1,
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"level",
|
||||||
|
"approved_by"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"escalation": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"max_consecutive_deviations": {
|
||||||
|
"type": "integer",
|
||||||
|
"exclusiveMinimum": 0,
|
||||||
|
"maximum": 9007199254740991
|
||||||
|
},
|
||||||
|
"deviation_threshold_pct": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?\\d+(\\.\\d+)?$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"max_consecutive_deviations",
|
||||||
|
"deviation_threshold_pct"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"ACTIVE",
|
||||||
|
"SUSPENDED",
|
||||||
|
"EXPIRED"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"scope",
|
||||||
|
"bounds",
|
||||||
|
"validity",
|
||||||
|
"approval",
|
||||||
|
"escalation",
|
||||||
|
"status"
|
||||||
|
],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"$id": "https://vpp-ai-platform/contracts/envelope.json",
|
||||||
|
"title": "Envelope"
|
||||||
|
}
|
||||||
44
contracts/schema/event_envelope.json
Normal file
44
contracts/schema/event_envelope.json
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"event_id": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"event_type": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"schema_version": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^\\d+\\.\\d+\\.\\d+$"
|
||||||
|
},
|
||||||
|
"occurred_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||||
|
},
|
||||||
|
"causation_id": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"correlation_id": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"payload": {}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"event_id",
|
||||||
|
"event_type",
|
||||||
|
"schema_version",
|
||||||
|
"occurred_at",
|
||||||
|
"causation_id",
|
||||||
|
"correlation_id",
|
||||||
|
"payload"
|
||||||
|
],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"$id": "https://vpp-ai-platform/contracts/event_envelope.json",
|
||||||
|
"title": "EventEnvelope"
|
||||||
|
}
|
||||||
62
contracts/schema/execution_permit.json
Normal file
62
contracts/schema/execution_permit.json
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"proposal_digest": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9a-f]{64}$"
|
||||||
|
},
|
||||||
|
"issued_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||||
|
},
|
||||||
|
"effect_limits": {
|
||||||
|
"type": "object",
|
||||||
|
"propertyNames": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?\\d+(\\.\\d+)?$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"revoked_at": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"issuer": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "authority-service"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"proposal_digest",
|
||||||
|
"issued_at",
|
||||||
|
"expires_at",
|
||||||
|
"effect_limits",
|
||||||
|
"revoked_at",
|
||||||
|
"issuer"
|
||||||
|
],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"$id": "https://vpp-ai-platform/contracts/execution_permit.json",
|
||||||
|
"title": "ExecutionPermit"
|
||||||
|
}
|
||||||
120
contracts/schema/flexibility_envelope.json
Normal file
120
contracts/schema/flexibility_envelope.json
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"provider": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"unit_id": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"SITE",
|
||||||
|
"AGGREGATION_UNIT",
|
||||||
|
"PROVINCE"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"unit_id",
|
||||||
|
"level"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"market_date": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
|
||||||
|
},
|
||||||
|
"up_mw": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"interval_minutes": {
|
||||||
|
"type": "number",
|
||||||
|
"const": 15
|
||||||
|
},
|
||||||
|
"date": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
|
||||||
|
},
|
||||||
|
"values": {
|
||||||
|
"minItems": 96,
|
||||||
|
"maxItems": 96,
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?\\d+(\\.\\d+)?$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"interval_minutes",
|
||||||
|
"date",
|
||||||
|
"values"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"down_mw": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"interval_minutes": {
|
||||||
|
"type": "number",
|
||||||
|
"const": 15
|
||||||
|
},
|
||||||
|
"date": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
|
||||||
|
},
|
||||||
|
"values": {
|
||||||
|
"minItems": 96,
|
||||||
|
"maxItems": 96,
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?\\d+(\\.\\d+)?$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"interval_minutes",
|
||||||
|
"date",
|
||||||
|
"values"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"valid_until": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||||
|
},
|
||||||
|
"schema_version": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"signature": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"provider",
|
||||||
|
"market_date",
|
||||||
|
"up_mw",
|
||||||
|
"down_mw",
|
||||||
|
"valid_until",
|
||||||
|
"schema_version",
|
||||||
|
"signature"
|
||||||
|
],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"$id": "https://vpp-ai-platform/contracts/flexibility_envelope.json",
|
||||||
|
"title": "FlexibilityEnvelope"
|
||||||
|
}
|
||||||
164
contracts/schema/forecast_bundle.json
Normal file
164
contracts/schema/forecast_bundle.json
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"LOAD",
|
||||||
|
"PV",
|
||||||
|
"PRICE"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"market_date": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"mw",
|
||||||
|
"yuan_per_mwh"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"quantiles": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"p10": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"interval_minutes": {
|
||||||
|
"type": "number",
|
||||||
|
"const": 15
|
||||||
|
},
|
||||||
|
"date": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
|
||||||
|
},
|
||||||
|
"values": {
|
||||||
|
"minItems": 96,
|
||||||
|
"maxItems": 96,
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?\\d+(\\.\\d+)?$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"interval_minutes",
|
||||||
|
"date",
|
||||||
|
"values"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"p50": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"interval_minutes": {
|
||||||
|
"type": "number",
|
||||||
|
"const": 15
|
||||||
|
},
|
||||||
|
"date": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
|
||||||
|
},
|
||||||
|
"values": {
|
||||||
|
"minItems": 96,
|
||||||
|
"maxItems": 96,
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?\\d+(\\.\\d+)?$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"interval_minutes",
|
||||||
|
"date",
|
||||||
|
"values"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"p90": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"interval_minutes": {
|
||||||
|
"type": "number",
|
||||||
|
"const": 15
|
||||||
|
},
|
||||||
|
"date": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
|
||||||
|
},
|
||||||
|
"values": {
|
||||||
|
"minItems": 96,
|
||||||
|
"maxItems": 96,
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?\\d+(\\.\\d+)?$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"interval_minutes",
|
||||||
|
"date",
|
||||||
|
"values"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"p10",
|
||||||
|
"p50",
|
||||||
|
"p90"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"version"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"features_snapshot_ref": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9a-f]{64}$"
|
||||||
|
},
|
||||||
|
"generated_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"kind",
|
||||||
|
"market_date",
|
||||||
|
"unit",
|
||||||
|
"quantiles",
|
||||||
|
"model",
|
||||||
|
"features_snapshot_ref",
|
||||||
|
"generated_at"
|
||||||
|
],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"$id": "https://vpp-ai-platform/contracts/forecast_bundle.json",
|
||||||
|
"title": "ForecastBundle"
|
||||||
|
}
|
||||||
112
contracts/schema/ledger_view.json
Normal file
112
contracts/schema/ledger_view.json
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"version": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 9007199254740991
|
||||||
|
},
|
||||||
|
"entries": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"timescale": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"ANNUAL",
|
||||||
|
"MONTHLY",
|
||||||
|
"DAY_AHEAD",
|
||||||
|
"INTRADAY",
|
||||||
|
"REALTIME"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"period": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^\\d{4}(-\\d{2}(-\\d{2})?)?$"
|
||||||
|
},
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"CONTRACT",
|
||||||
|
"BID_SUBMITTED",
|
||||||
|
"AWARD",
|
||||||
|
"EXTERNAL_COMMITMENT"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"energy_mwh": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?\\d+(\\.\\d+)?$"
|
||||||
|
},
|
||||||
|
"curve": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"interval_minutes": {
|
||||||
|
"type": "number",
|
||||||
|
"const": 15
|
||||||
|
},
|
||||||
|
"date": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
|
||||||
|
},
|
||||||
|
"values": {
|
||||||
|
"minItems": 96,
|
||||||
|
"maxItems": 96,
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?\\d+(\\.\\d+)?$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"interval_minutes",
|
||||||
|
"date",
|
||||||
|
"values"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"source_ref": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"recorded_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"timescale",
|
||||||
|
"period",
|
||||||
|
"kind",
|
||||||
|
"energy_mwh",
|
||||||
|
"curve",
|
||||||
|
"source_ref",
|
||||||
|
"recorded_at"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"version",
|
||||||
|
"entries"
|
||||||
|
],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"$id": "https://vpp-ai-platform/contracts/ledger_view.json",
|
||||||
|
"title": "LedgerView"
|
||||||
|
}
|
||||||
94
contracts/schema/position_update.json
Normal file
94
contracts/schema/position_update.json
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"timescale": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"ANNUAL",
|
||||||
|
"MONTHLY",
|
||||||
|
"DAY_AHEAD",
|
||||||
|
"INTRADAY",
|
||||||
|
"REALTIME"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"period": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^\\d{4}(-\\d{2}(-\\d{2})?)?$"
|
||||||
|
},
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"CONTRACT",
|
||||||
|
"BID_SUBMITTED",
|
||||||
|
"AWARD",
|
||||||
|
"EXTERNAL_COMMITMENT"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"energy_mwh": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?\\d+(\\.\\d+)?$"
|
||||||
|
},
|
||||||
|
"curve": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"interval_minutes": {
|
||||||
|
"type": "number",
|
||||||
|
"const": 15
|
||||||
|
},
|
||||||
|
"date": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
|
||||||
|
},
|
||||||
|
"values": {
|
||||||
|
"minItems": 96,
|
||||||
|
"maxItems": 96,
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?\\d+(\\.\\d+)?$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"interval_minutes",
|
||||||
|
"date",
|
||||||
|
"values"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"source_ref": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"expected_version": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 9007199254740991
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"timescale",
|
||||||
|
"period",
|
||||||
|
"kind",
|
||||||
|
"energy_mwh",
|
||||||
|
"curve",
|
||||||
|
"source_ref",
|
||||||
|
"expected_version"
|
||||||
|
],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"$id": "https://vpp-ai-platform/contracts/position_update.json",
|
||||||
|
"title": "PositionUpdate"
|
||||||
|
}
|
||||||
255
contracts/schema/proposal.json
Normal file
255
contracts/schema/proposal.json
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"timescale": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"ANNUAL",
|
||||||
|
"MONTHLY",
|
||||||
|
"DAY_AHEAD",
|
||||||
|
"INTRADAY",
|
||||||
|
"REALTIME"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"originator": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"agent": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"trigger": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"MANUAL",
|
||||||
|
"EVENT",
|
||||||
|
"SCHEDULED"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"task_id": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"agent",
|
||||||
|
"trigger",
|
||||||
|
"task_id"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"lineage": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"tool_calls": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"tool_call_id": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"tool": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"inputs_ref": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9a-f]{64}$"
|
||||||
|
},
|
||||||
|
"outputs_ref": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9a-f]{64}$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"tool_call_id",
|
||||||
|
"tool",
|
||||||
|
"version",
|
||||||
|
"inputs_ref",
|
||||||
|
"outputs_ref"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"data_refs": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9a-f]{64}$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ledger_version": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 9007199254740991
|
||||||
|
},
|
||||||
|
"policy_pack_version": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"tool_calls",
|
||||||
|
"data_refs",
|
||||||
|
"ledger_version",
|
||||||
|
"policy_pack_version"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"envelope_ref": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"digest": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9a-f]{64}$"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"DRAFT",
|
||||||
|
"RULE_CHECK",
|
||||||
|
"SIMULATION",
|
||||||
|
"ENVELOPE_CHECK",
|
||||||
|
"PENDING_HUMAN",
|
||||||
|
"AUTO_APPROVED",
|
||||||
|
"APPROVED",
|
||||||
|
"FRESH_CHECK",
|
||||||
|
"STALE",
|
||||||
|
"AUTHORIZED",
|
||||||
|
"RELEASED",
|
||||||
|
"EXECUTING",
|
||||||
|
"COMPLETED",
|
||||||
|
"ROLLED_BACK",
|
||||||
|
"REJECTED"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "BID"
|
||||||
|
},
|
||||||
|
"payload": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "BID"
|
||||||
|
},
|
||||||
|
"market_date": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
|
||||||
|
},
|
||||||
|
"prices_yuan_per_mwh": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"interval_minutes": {
|
||||||
|
"type": "number",
|
||||||
|
"const": 15
|
||||||
|
},
|
||||||
|
"date": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
|
||||||
|
},
|
||||||
|
"values": {
|
||||||
|
"minItems": 96,
|
||||||
|
"maxItems": 96,
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?\\d+(\\.\\d+)?$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"interval_minutes",
|
||||||
|
"date",
|
||||||
|
"values"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"quantities_mwh": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"interval_minutes": {
|
||||||
|
"type": "number",
|
||||||
|
"const": 15
|
||||||
|
},
|
||||||
|
"date": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
|
||||||
|
},
|
||||||
|
"values": {
|
||||||
|
"minItems": 96,
|
||||||
|
"maxItems": 96,
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?\\d+(\\.\\d+)?$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"interval_minutes",
|
||||||
|
"date",
|
||||||
|
"values"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"expected_revenue_yuan": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?\\d+(\\.\\d+)?$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"kind",
|
||||||
|
"market_date",
|
||||||
|
"prices_yuan_per_mwh",
|
||||||
|
"quantities_mwh",
|
||||||
|
"expected_revenue_yuan"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"timescale",
|
||||||
|
"originator",
|
||||||
|
"lineage",
|
||||||
|
"envelope_ref",
|
||||||
|
"digest",
|
||||||
|
"status",
|
||||||
|
"created_at",
|
||||||
|
"type",
|
||||||
|
"payload"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"$id": "https://vpp-ai-platform/contracts/proposal.json",
|
||||||
|
"title": "Proposal"
|
||||||
|
}
|
||||||
102
contracts/schema/resource_profile.json
Normal file
102
contracts/schema/resource_profile.json
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"resource_id": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"STORAGE",
|
||||||
|
"PV",
|
||||||
|
"EV_CHARGER",
|
||||||
|
"HVAC",
|
||||||
|
"INDUSTRIAL_LOAD",
|
||||||
|
"MICROGRID",
|
||||||
|
"BASE_STATION"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"rated_power_mw": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?\\d+(\\.\\d+)?$"
|
||||||
|
},
|
||||||
|
"certified_adjustable_mw": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?\\d+(\\.\\d+)?$"
|
||||||
|
},
|
||||||
|
"confidence": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?\\d+(\\.\\d+)?$"
|
||||||
|
},
|
||||||
|
"reliability_score": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?\\d+(\\.\\d+)?$"
|
||||||
|
},
|
||||||
|
"constraints": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"min_duration_min": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"exclusiveMinimum": 0,
|
||||||
|
"maximum": 9007199254740991
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"recovery_rate_mw_per_min": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^-?\\d+(\\.\\d+)?$"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"min_duration_min",
|
||||||
|
"recovery_rate_mw_per_min"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"evidence_refs": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9a-f]{64}$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"resource_id",
|
||||||
|
"name",
|
||||||
|
"type",
|
||||||
|
"rated_power_mw",
|
||||||
|
"certified_adjustable_mw",
|
||||||
|
"confidence",
|
||||||
|
"reliability_score",
|
||||||
|
"constraints",
|
||||||
|
"evidence_refs",
|
||||||
|
"updated_at"
|
||||||
|
],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"$id": "https://vpp-ai-platform/contracts/resource_profile.json",
|
||||||
|
"title": "ResourceProfile"
|
||||||
|
}
|
||||||
80
contracts/schema/situation_report.json
Normal file
80
contracts/schema/situation_report.json
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"market_date": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
|
||||||
|
},
|
||||||
|
"risk_level": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"LOW",
|
||||||
|
"MEDIUM",
|
||||||
|
"HIGH",
|
||||||
|
"EXTREME"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"findings": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"TREND",
|
||||||
|
"ANOMALY",
|
||||||
|
"RISK",
|
||||||
|
"ATTRIBUTION"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"summary": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"refs": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9a-f]{64}$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"kind",
|
||||||
|
"summary",
|
||||||
|
"refs"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"forecast_refs": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"generated_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time",
|
||||||
|
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"market_date",
|
||||||
|
"risk_level",
|
||||||
|
"findings",
|
||||||
|
"forecast_refs",
|
||||||
|
"generated_at"
|
||||||
|
],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"$id": "https://vpp-ai-platform/contracts/situation_report.json",
|
||||||
|
"title": "SituationReport"
|
||||||
|
}
|
||||||
1700
package-lock.json
generated
Normal file
1700
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
14
package.json
Normal file
14
package.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "vpp-ai-platform",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"workspaces": [
|
||||||
|
"packages/*"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"test": "npm run test --workspaces --if-present",
|
||||||
|
"export:schemas": "npm run export:schemas -w @vpp/domain",
|
||||||
|
"make:fixtures": "npm run make:fixtures -w @vpp/domain",
|
||||||
|
"check": "npm run export:schemas && npm test"
|
||||||
|
}
|
||||||
|
}
|
||||||
23
packages/domain/package.json
Normal file
23
packages/domain/package.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "@vpp/domain",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "vitest run",
|
||||||
|
"export:schemas": "tsx scripts/export-schemas.ts",
|
||||||
|
"make:fixtures": "tsx scripts/make-fixtures.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"zod": "^4.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^26.4.1",
|
||||||
|
"tsx": "^4.19.0",
|
||||||
|
"typescript": "^5.6.0",
|
||||||
|
"vitest": "^3.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
22
packages/domain/scripts/export-schemas.ts
Normal file
22
packages/domain/scripts/export-schemas.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Exports every registry schema to contracts/schema/<name>.json (JSON Schema,
|
||||||
|
* draft 2020-12). These committed files are the neutral cross-language contract
|
||||||
|
* (ADR-0002); the Python side generates pydantic models from them.
|
||||||
|
*/
|
||||||
|
import { mkdirSync, writeFileSync } from 'node:fs'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { schemaRegistry } from '../src/index.js'
|
||||||
|
|
||||||
|
const outDir = fileURLToPath(new URL('../../../contracts/schema/', import.meta.url))
|
||||||
|
mkdirSync(outDir, { recursive: true })
|
||||||
|
|
||||||
|
const pascal = (s: string) => s.split('_').map((w) => w[0]!.toUpperCase() + w.slice(1)).join('')
|
||||||
|
|
||||||
|
for (const [name, schema] of Object.entries(schemaRegistry)) {
|
||||||
|
const json = z.toJSONSchema(schema) as Record<string, unknown>
|
||||||
|
json['$id'] = `https://vpp-ai-platform/contracts/${name}.json`
|
||||||
|
json['title'] = pascal(name) // becomes the pydantic class name
|
||||||
|
writeFileSync(`${outDir}${name}.json`, JSON.stringify(json, null, 2) + '\n')
|
||||||
|
console.log(`exported ${name}.json`)
|
||||||
|
}
|
||||||
244
packages/domain/scripts/make-fixtures.ts
Normal file
244
packages/domain/scripts/make-fixtures.ts
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
/**
|
||||||
|
* 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')
|
||||||
38
packages/domain/src/approval.ts
Normal file
38
packages/domain/src/approval.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
import { DecimalString, Id, IsoUtc, SnapshotRef } from './common.js'
|
||||||
|
import { ProposalType } from './proposal.js'
|
||||||
|
|
||||||
|
export const ApprovalLevel = z.enum(['L1', 'L2', 'L3'])
|
||||||
|
|
||||||
|
/** Binds to an exact proposal digest and validity window; stale when evidence changes (docs/03 §2.1). */
|
||||||
|
export const Approval = z.object({
|
||||||
|
id: Id,
|
||||||
|
proposal_digest: SnapshotRef,
|
||||||
|
decision: z.enum(['APPROVE', 'REJECT']),
|
||||||
|
approver: z.object({ id: Id, role: z.string().min(1) }),
|
||||||
|
scope: z.object({
|
||||||
|
effect_type: ProposalType,
|
||||||
|
limits: z.record(z.string(), DecimalString),
|
||||||
|
}),
|
||||||
|
validity: z.object({ from: IsoUtc, to: IsoUtc }),
|
||||||
|
evidence_versions: z.object({
|
||||||
|
policy_pack_version: z.string().min(1),
|
||||||
|
ledger_version: z.int().nonnegative(),
|
||||||
|
data_snapshot_refs: z.array(SnapshotRef),
|
||||||
|
}),
|
||||||
|
comment: z.string().optional(),
|
||||||
|
decided_at: IsoUtc,
|
||||||
|
})
|
||||||
|
export type Approval = z.infer<typeof Approval>
|
||||||
|
|
||||||
|
/** Short-lived, revocable. Gateways accept only (Proposal, Permit) pairs (I4, docs/03 §2.2). */
|
||||||
|
export const ExecutionPermit = z.object({
|
||||||
|
id: Id,
|
||||||
|
proposal_digest: SnapshotRef,
|
||||||
|
issued_at: IsoUtc,
|
||||||
|
expires_at: IsoUtc,
|
||||||
|
effect_limits: z.record(z.string(), DecimalString),
|
||||||
|
revoked_at: IsoUtc.nullable(),
|
||||||
|
issuer: z.literal('authority-service'),
|
||||||
|
})
|
||||||
|
export type ExecutionPermit = z.infer<typeof ExecutionPermit>
|
||||||
28
packages/domain/src/case.ts
Normal file
28
packages/domain/src/case.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
import { Id, IsoUtc } from './common.js'
|
||||||
|
|
||||||
|
export const CaseKind = z.enum(['DAY_AHEAD_BID', 'DR_EVENT', 'DEVIATION_RESOLUTION', 'REVIEW', 'ADHOC_ANALYSIS'])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Operator's unit of accountability (docs/02 §5, ADR-0008): one objective,
|
||||||
|
* one owner, one deadline, a completion contract. References, never copies.
|
||||||
|
*/
|
||||||
|
export const DecisionCase = z.object({
|
||||||
|
id: Id,
|
||||||
|
kind: CaseKind,
|
||||||
|
objective: z.string().min(1),
|
||||||
|
owner: Id,
|
||||||
|
deadline: IsoUtc,
|
||||||
|
status: z.enum(['OPEN', 'IN_PROGRESS', 'AWAITING_APPROVAL', 'CLOSED_DONE', 'CLOSED_ABORTED']),
|
||||||
|
refs: z.object({
|
||||||
|
evidence: z.array(Id),
|
||||||
|
proposals: z.array(Id),
|
||||||
|
approvals: z.array(Id),
|
||||||
|
permits: z.array(Id),
|
||||||
|
scenarios: z.array(Id),
|
||||||
|
outcome: Id.nullable(),
|
||||||
|
}),
|
||||||
|
opened_at: IsoUtc,
|
||||||
|
closed_at: IsoUtc.nullable(),
|
||||||
|
})
|
||||||
|
export type DecisionCase = z.infer<typeof DecisionCase>
|
||||||
35
packages/domain/src/common.ts
Normal file
35
packages/domain/src/common.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fixed-point decimal encoded as a string. Money/energy/prices must never be
|
||||||
|
* JS numbers or JSON floats (docs/11 §3.3) — TS `number` and Python `float`
|
||||||
|
* disagree exactly where settlement reconciliation cares.
|
||||||
|
*/
|
||||||
|
export const DecimalString = z
|
||||||
|
.string()
|
||||||
|
.regex(/^-?\d+(\.\d+)?$/, 'fixed-point decimal encoded as string')
|
||||||
|
export type DecimalString = z.infer<typeof DecimalString>
|
||||||
|
|
||||||
|
/** ISO8601 UTC timestamp. Market-interval semantics use {date, interval} instead. */
|
||||||
|
export const IsoUtc = z.iso.datetime()
|
||||||
|
|
||||||
|
/** Market date in Asia/Shanghai semantics (docs/11 §3.3). */
|
||||||
|
export const MarketDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/)
|
||||||
|
export const MarketMonth = z.string().regex(/^\d{4}-\d{2}$/)
|
||||||
|
|
||||||
|
export const Id = z.string().min(1)
|
||||||
|
|
||||||
|
/** sha256 hex reference to an immutable content-addressed snapshot. */
|
||||||
|
export const SnapshotRef = z.string().regex(/^[0-9a-f]{64}$/)
|
||||||
|
export type SnapshotRef = z.infer<typeof SnapshotRef>
|
||||||
|
|
||||||
|
export const Timescale = z.enum(['ANNUAL', 'MONTHLY', 'DAY_AHEAD', 'INTRADAY', 'REALTIME'])
|
||||||
|
export type Timescale = z.infer<typeof Timescale>
|
||||||
|
|
||||||
|
/** 96 × 15-min values for one market date; index 0 = 00:00–00:15 Asia/Shanghai. */
|
||||||
|
export const Curve96 = z.object({
|
||||||
|
interval_minutes: z.literal(15),
|
||||||
|
date: MarketDate,
|
||||||
|
values: z.array(DecimalString).length(96),
|
||||||
|
})
|
||||||
|
export type Curve96 = z.infer<typeof Curve96>
|
||||||
48
packages/domain/src/envelope.ts
Normal file
48
packages/domain/src/envelope.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
import { Curve96, DecimalString, Id, IsoUtc, MarketDate, Timescale } from './common.js'
|
||||||
|
import { ApprovalLevel } from './approval.js'
|
||||||
|
import { ProposalType } from './proposal.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authority envelope (授权包络, downward): human-pre-approved autonomy bounds.
|
||||||
|
* Bound keys are type-specific and documented in docs/03 §3; initial values are
|
||||||
|
* OPEN-QUESTION B1/B2 — never hardcode them.
|
||||||
|
*/
|
||||||
|
export const Envelope = z.object({
|
||||||
|
id: Id,
|
||||||
|
scope: z.object({
|
||||||
|
proposal_type: ProposalType,
|
||||||
|
timescales: z.array(Timescale).min(1),
|
||||||
|
resource_set: Id,
|
||||||
|
}),
|
||||||
|
bounds: z.record(z.string(), DecimalString),
|
||||||
|
validity: z.object({ from: IsoUtc, to: IsoUtc }),
|
||||||
|
approval: z.object({ level: ApprovalLevel, approved_by: z.array(Id).min(1) }),
|
||||||
|
escalation: z.object({
|
||||||
|
max_consecutive_deviations: z.int().positive(),
|
||||||
|
deviation_threshold_pct: DecimalString,
|
||||||
|
}),
|
||||||
|
status: z.enum(['ACTIVE', 'SUSPENDED', 'EXPIRED']),
|
||||||
|
})
|
||||||
|
export type Envelope = z.infer<typeof Envelope>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Capability envelope (灵活性包络, upward): adjustable capacity report from an
|
||||||
|
* aggregation unit — the same schema crosses the federation boundary in
|
||||||
|
* phase 2 (docs/10 §5, no-rework reservation #1).
|
||||||
|
*/
|
||||||
|
export const FlexibilityEnvelope = z.object({
|
||||||
|
id: Id,
|
||||||
|
provider: z.object({
|
||||||
|
unit_id: Id,
|
||||||
|
level: z.enum(['SITE', 'AGGREGATION_UNIT', 'PROVINCE']),
|
||||||
|
}),
|
||||||
|
market_date: MarketDate,
|
||||||
|
up_mw: Curve96,
|
||||||
|
down_mw: Curve96,
|
||||||
|
valid_until: IsoUtc,
|
||||||
|
schema_version: z.string().min(1),
|
||||||
|
/** Signing arrives with federation (phase 2); null within-province. */
|
||||||
|
signature: z.string().nullable(),
|
||||||
|
})
|
||||||
|
export type FlexibilityEnvelope = z.infer<typeof FlexibilityEnvelope>
|
||||||
17
packages/domain/src/event.ts
Normal file
17
packages/domain/src/event.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
import { Id, IsoUtc } from './common.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Typed event wrapper for the outbox/bus (docs/11 §1.3). causation/correlation
|
||||||
|
* ids let audit and review walk causal chains through the event log.
|
||||||
|
*/
|
||||||
|
export const EventEnvelope = z.object({
|
||||||
|
event_id: Id,
|
||||||
|
event_type: z.string().min(1),
|
||||||
|
schema_version: z.string().regex(/^\d+\.\d+\.\d+$/),
|
||||||
|
occurred_at: IsoUtc,
|
||||||
|
causation_id: Id,
|
||||||
|
correlation_id: Id,
|
||||||
|
payload: z.unknown(),
|
||||||
|
})
|
||||||
|
export type EventEnvelope = z.infer<typeof EventEnvelope>
|
||||||
22
packages/domain/src/forecast.ts
Normal file
22
packages/domain/src/forecast.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
import { Curve96, Id, IsoUtc, MarketDate, SnapshotRef } from './common.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forecast output contract (docs/05 §2.1): quantile intervals are mandatory —
|
||||||
|
* bid risk assessment depends on calibrated bands, not point values.
|
||||||
|
*/
|
||||||
|
export const ForecastBundle = z.object({
|
||||||
|
id: Id,
|
||||||
|
kind: z.enum(['LOAD', 'PV', 'PRICE']),
|
||||||
|
market_date: MarketDate,
|
||||||
|
unit: z.enum(['mw', 'yuan_per_mwh']),
|
||||||
|
quantiles: z.object({
|
||||||
|
p10: Curve96,
|
||||||
|
p50: Curve96,
|
||||||
|
p90: Curve96,
|
||||||
|
}),
|
||||||
|
model: z.object({ name: z.string().min(1), version: z.string().min(1) }),
|
||||||
|
features_snapshot_ref: SnapshotRef,
|
||||||
|
generated_at: IsoUtc,
|
||||||
|
})
|
||||||
|
export type ForecastBundle = z.infer<typeof ForecastBundle>
|
||||||
40
packages/domain/src/index.ts
Normal file
40
packages/domain/src/index.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
import { Approval, ExecutionPermit } from './approval.js'
|
||||||
|
import { DecisionCase } from './case.js'
|
||||||
|
import { Envelope, FlexibilityEnvelope } from './envelope.js'
|
||||||
|
import { EventEnvelope } from './event.js'
|
||||||
|
import { ForecastBundle } from './forecast.js'
|
||||||
|
import { LedgerView, PositionUpdate } from './ledger.js'
|
||||||
|
import { Proposal } from './proposal.js'
|
||||||
|
import { ResourceProfile } from './resource.js'
|
||||||
|
import { SituationReport } from './situation.js'
|
||||||
|
|
||||||
|
export * from './common.js'
|
||||||
|
export * from './proposal.js'
|
||||||
|
export * from './approval.js'
|
||||||
|
export * from './envelope.js'
|
||||||
|
export * from './ledger.js'
|
||||||
|
export * from './forecast.js'
|
||||||
|
export * from './situation.js'
|
||||||
|
export * from './resource.js'
|
||||||
|
export * from './case.js'
|
||||||
|
export * from './event.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registry driving the contracts pipeline: keys become schema/fixture/module
|
||||||
|
* names on both the TS and Python sides (underscores — Python module names).
|
||||||
|
*/
|
||||||
|
export const schemaRegistry: Record<string, z.ZodType> = {
|
||||||
|
proposal: Proposal,
|
||||||
|
approval: Approval,
|
||||||
|
execution_permit: ExecutionPermit,
|
||||||
|
envelope: Envelope,
|
||||||
|
flexibility_envelope: FlexibilityEnvelope,
|
||||||
|
position_update: PositionUpdate,
|
||||||
|
ledger_view: LedgerView,
|
||||||
|
forecast_bundle: ForecastBundle,
|
||||||
|
situation_report: SituationReport,
|
||||||
|
resource_profile: ResourceProfile,
|
||||||
|
decision_case: DecisionCase,
|
||||||
|
event_envelope: EventEnvelope,
|
||||||
|
}
|
||||||
32
packages/domain/src/ledger.ts
Normal file
32
packages/domain/src/ledger.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
import { Curve96, DecimalString, Id, IsoUtc, Timescale } from './common.js'
|
||||||
|
|
||||||
|
export const PositionKind = z.enum(['CONTRACT', 'BID_SUBMITTED', 'AWARD', 'EXTERNAL_COMMITMENT'])
|
||||||
|
|
||||||
|
/** Period granularity follows timescale: YYYY | YYYY-MM | YYYY-MM-DD. */
|
||||||
|
const Period = z.string().regex(/^\d{4}(-\d{2}(-\d{2})?)?$/)
|
||||||
|
|
||||||
|
export const PositionUpdate = z.object({
|
||||||
|
id: Id,
|
||||||
|
timescale: Timescale,
|
||||||
|
period: Period,
|
||||||
|
kind: PositionKind,
|
||||||
|
energy_mwh: DecimalString,
|
||||||
|
curve: Curve96.nullable(),
|
||||||
|
/** Proposal digest, settlement id, or contract id that caused this entry. */
|
||||||
|
source_ref: z.string().min(1),
|
||||||
|
/** Optimistic concurrency: append fails unless this matches current version. */
|
||||||
|
expected_version: z.int().nonnegative(),
|
||||||
|
})
|
||||||
|
export type PositionUpdate = z.infer<typeof PositionUpdate>
|
||||||
|
|
||||||
|
export const PositionEntry = PositionUpdate.omit({ expected_version: true }).extend({
|
||||||
|
recorded_at: IsoUtc,
|
||||||
|
})
|
||||||
|
export type PositionEntry = z.infer<typeof PositionEntry>
|
||||||
|
|
||||||
|
export const LedgerView = z.object({
|
||||||
|
version: z.int().nonnegative(),
|
||||||
|
entries: z.array(PositionEntry),
|
||||||
|
})
|
||||||
|
export type LedgerView = z.infer<typeof LedgerView>
|
||||||
75
packages/domain/src/proposal.ts
Normal file
75
packages/domain/src/proposal.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
import { Curve96, DecimalString, Id, IsoUtc, MarketDate, SnapshotRef, Timescale } from './common.js'
|
||||||
|
|
||||||
|
export const ProposalType = z.enum(['BID', 'INVITATION', 'CONTROL_PLAN', 'DISPATCH_PLAN'])
|
||||||
|
export type ProposalType = z.infer<typeof ProposalType>
|
||||||
|
|
||||||
|
export const TriggerKind = z.enum(['MANUAL', 'EVENT', 'SCHEDULED'])
|
||||||
|
|
||||||
|
/** Full lifecycle per docs/03 §2 (incl. FRESH_CHECK/STALE/AUTHORIZED from the staleness model). */
|
||||||
|
export const ProposalStatus = z.enum([
|
||||||
|
'DRAFT',
|
||||||
|
'RULE_CHECK',
|
||||||
|
'SIMULATION',
|
||||||
|
'ENVELOPE_CHECK',
|
||||||
|
'PENDING_HUMAN',
|
||||||
|
'AUTO_APPROVED',
|
||||||
|
'APPROVED',
|
||||||
|
'FRESH_CHECK',
|
||||||
|
'STALE',
|
||||||
|
'AUTHORIZED',
|
||||||
|
'RELEASED',
|
||||||
|
'EXECUTING',
|
||||||
|
'COMPLETED',
|
||||||
|
'ROLLED_BACK',
|
||||||
|
'REJECTED',
|
||||||
|
])
|
||||||
|
|
||||||
|
export const ToolCallRef = z.object({
|
||||||
|
tool_call_id: Id,
|
||||||
|
tool: Id,
|
||||||
|
version: z.string().min(1),
|
||||||
|
inputs_ref: SnapshotRef,
|
||||||
|
outputs_ref: SnapshotRef,
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provenance attached to every proposal (docs/03 §1). Rule check enforces that
|
||||||
|
* every numeric payload field traces to one of these tool calls (invariant/P2).
|
||||||
|
*/
|
||||||
|
export const Lineage = z.object({
|
||||||
|
tool_calls: z.array(ToolCallRef),
|
||||||
|
data_refs: z.array(SnapshotRef),
|
||||||
|
ledger_version: z.int().nonnegative(),
|
||||||
|
policy_pack_version: z.string().min(1),
|
||||||
|
})
|
||||||
|
|
||||||
|
/** Day-ahead bid: per-interval price/quantity pairs. */
|
||||||
|
export const BidPayload = z.object({
|
||||||
|
kind: z.literal('BID'),
|
||||||
|
market_date: MarketDate,
|
||||||
|
prices_yuan_per_mwh: Curve96,
|
||||||
|
quantities_mwh: Curve96,
|
||||||
|
expected_revenue_yuan: DecimalString,
|
||||||
|
})
|
||||||
|
|
||||||
|
const ProposalBase = z.object({
|
||||||
|
id: Id,
|
||||||
|
timescale: Timescale,
|
||||||
|
originator: z.object({ agent: Id, trigger: TriggerKind, task_id: Id }),
|
||||||
|
lineage: Lineage,
|
||||||
|
envelope_ref: Id.nullable(),
|
||||||
|
/** sha256 over canonical payload + lineage. Approvals and permits bind to this (I3/I4). */
|
||||||
|
digest: SnapshotRef,
|
||||||
|
status: ProposalStatus,
|
||||||
|
created_at: IsoUtc,
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* M1 carries the BID variant only; INVITATION / CONTROL_PLAN / DISPATCH_PLAN
|
||||||
|
* payloads arrive with their milestones (ROADMAP M4 / phase 2).
|
||||||
|
*/
|
||||||
|
export const Proposal = z.discriminatedUnion('type', [
|
||||||
|
ProposalBase.extend({ type: z.literal('BID'), payload: BidPayload }),
|
||||||
|
])
|
||||||
|
export type Proposal = z.infer<typeof Proposal>
|
||||||
36
packages/domain/src/resource.ts
Normal file
36
packages/domain/src/resource.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
import { DecimalString, Id, IsoUtc, SnapshotRef } from './common.js'
|
||||||
|
|
||||||
|
export const ResourceType = z.enum([
|
||||||
|
'STORAGE',
|
||||||
|
'PV',
|
||||||
|
'EV_CHARGER',
|
||||||
|
'HVAC',
|
||||||
|
'INDUSTRIAL_LOAD',
|
||||||
|
'MICROGRID',
|
||||||
|
'BASE_STATION',
|
||||||
|
])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resource profile (docs/02 §2.2). reliability_score is written back by the
|
||||||
|
* review loop; certified capacity must reference recent execution evidence
|
||||||
|
* (profile staleness is top-3 risk, docs/13 §0).
|
||||||
|
*/
|
||||||
|
export const ResourceProfile = z.object({
|
||||||
|
resource_id: Id,
|
||||||
|
name: z.string().min(1),
|
||||||
|
type: ResourceType,
|
||||||
|
rated_power_mw: DecimalString,
|
||||||
|
certified_adjustable_mw: DecimalString,
|
||||||
|
/** 0–1 confidence in the certified figure. */
|
||||||
|
confidence: DecimalString,
|
||||||
|
/** 0–1, updated by ReviewFinding writebacks. */
|
||||||
|
reliability_score: DecimalString,
|
||||||
|
constraints: z.object({
|
||||||
|
min_duration_min: z.int().positive().nullable(),
|
||||||
|
recovery_rate_mw_per_min: DecimalString.nullable(),
|
||||||
|
}),
|
||||||
|
evidence_refs: z.array(SnapshotRef),
|
||||||
|
updated_at: IsoUtc,
|
||||||
|
})
|
||||||
|
export type ResourceProfile = z.infer<typeof ResourceProfile>
|
||||||
21
packages/domain/src/situation.ts
Normal file
21
packages/domain/src/situation.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
import { Id, IsoUtc, MarketDate, SnapshotRef } from './common.js'
|
||||||
|
|
||||||
|
export const RiskLevel = z.enum(['LOW', 'MEDIUM', 'HIGH', 'EXTREME'])
|
||||||
|
|
||||||
|
/** Analysis agent output (docs/02 §2.1). EXTREME triggers the abnormal-day protocol (docs/13 §1). */
|
||||||
|
export const SituationReport = z.object({
|
||||||
|
id: Id,
|
||||||
|
market_date: MarketDate,
|
||||||
|
risk_level: RiskLevel,
|
||||||
|
findings: z.array(
|
||||||
|
z.object({
|
||||||
|
kind: z.enum(['TREND', 'ANOMALY', 'RISK', 'ATTRIBUTION']),
|
||||||
|
summary: z.string().min(1),
|
||||||
|
refs: z.array(SnapshotRef),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
forecast_refs: z.array(Id),
|
||||||
|
generated_at: IsoUtc,
|
||||||
|
})
|
||||||
|
export type SituationReport = z.infer<typeof SituationReport>
|
||||||
41
packages/domain/test/fixtures.test.ts
Normal file
41
packages/domain/test/fixtures.test.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { readdirSync, readFileSync } from 'node:fs'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { schemaRegistry } from '../src/index.js'
|
||||||
|
|
||||||
|
const root = fileURLToPath(new URL('../../../contracts/', import.meta.url))
|
||||||
|
|
||||||
|
const casesIn = (dir: string): Array<[string, string, string]> => {
|
||||||
|
const out: Array<[string, string, string]> = []
|
||||||
|
for (const name of readdirSync(`${root}${dir}`)) {
|
||||||
|
for (const file of readdirSync(`${root}${dir}/${name}`)) {
|
||||||
|
out.push([name, file, `${root}${dir}/${name}/${file}`])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('golden fixtures validate against domain schemas', () => {
|
||||||
|
it.each(casesIn('fixtures'))('%s / %s', (name, _file, path) => {
|
||||||
|
const schema = schemaRegistry[name]
|
||||||
|
expect(schema, `no schema registered for fixture dir '${name}'`).toBeDefined()
|
||||||
|
const data = JSON.parse(readFileSync(path, 'utf8'))
|
||||||
|
const result = schema!.safeParse(data)
|
||||||
|
expect(result.success, JSON.stringify(result.error?.issues, null, 2)).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('invalid fixtures are rejected', () => {
|
||||||
|
it.each(casesIn('fixtures-invalid'))('%s / %s', (name, _file, path) => {
|
||||||
|
const schema = schemaRegistry[name]
|
||||||
|
expect(schema).toBeDefined()
|
||||||
|
const data = JSON.parse(readFileSync(path, 'utf8'))
|
||||||
|
expect(schema!.safeParse(data).success).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('every registered schema has at least one golden fixture', () => {
|
||||||
|
it.each(Object.keys(schemaRegistry))('%s', (name) => {
|
||||||
|
expect(readdirSync(`${root}fixtures/${name}`).length).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
8
packages/domain/tsconfig.json
Normal file
8
packages/domain/tsconfig.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": ".",
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"include": ["src", "scripts", "test"]
|
||||||
|
}
|
||||||
21
packages/services/package.json
Normal file
21
packages/services/package.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "@vpp/services",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "vitest run"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@vpp/domain": "*",
|
||||||
|
"decimal.js-light": "^2.5.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^26.4.1",
|
||||||
|
"typescript": "^5.6.0",
|
||||||
|
"vitest": "^3.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
3
packages/services/src/index.ts
Normal file
3
packages/services/src/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './snapshot.js'
|
||||||
|
export * from './ledger.js'
|
||||||
|
export * from './quality.js'
|
||||||
102
packages/services/src/ledger.ts
Normal file
102
packages/services/src/ledger.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { Decimal } from 'decimal.js-light'
|
||||||
|
import type { LedgerView, PositionEntry, PositionUpdate, Timescale } from '@vpp/domain'
|
||||||
|
import { PositionUpdate as PositionUpdateSchema } from '@vpp/domain'
|
||||||
|
|
||||||
|
export class LedgerConcurrencyError extends Error {
|
||||||
|
constructor(expected: number, actual: number) {
|
||||||
|
super(`ledger version mismatch: update expected ${expected}, current is ${actual}`)
|
||||||
|
this.name = 'LedgerConcurrencyError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CascadeViolation extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message)
|
||||||
|
this.name = 'CascadeViolation'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LedgerConfig {
|
||||||
|
/**
|
||||||
|
* Allowed relative deviation of a day-ahead bid's daily energy from the
|
||||||
|
* pro-rata daily share of the monthly contracted position.
|
||||||
|
* OPEN-QUESTION A5: the real Hubei decomposition-band rule may differ in
|
||||||
|
* shape (cumulative month-to-date band? asymmetric?) — see docs/open-questions.md.
|
||||||
|
*/
|
||||||
|
daMonthlyDeviationBand: string // e.g. "0.05" = ±5%
|
||||||
|
clock?: () => string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Position ledger v1 (docs/00 §4, P7): in-memory, optimistic concurrency,
|
||||||
|
* per-timescale views, and the constraint cascade — upper-timescale positions
|
||||||
|
* bound lower-timescale writes. Storage adapter (Postgres) arrives with M3.
|
||||||
|
*/
|
||||||
|
export class LedgerService {
|
||||||
|
private version = 0
|
||||||
|
private entries: PositionEntry[] = []
|
||||||
|
|
||||||
|
constructor(private readonly cfg: LedgerConfig) {}
|
||||||
|
|
||||||
|
read(): LedgerView {
|
||||||
|
return { version: this.version, entries: [...this.entries] }
|
||||||
|
}
|
||||||
|
|
||||||
|
viewByTimescale(timescale: Timescale): PositionEntry[] {
|
||||||
|
return this.entries.filter((e) => e.timescale === timescale)
|
||||||
|
}
|
||||||
|
|
||||||
|
append(update: PositionUpdate): LedgerView {
|
||||||
|
PositionUpdateSchema.parse(update)
|
||||||
|
if (update.expected_version !== this.version) {
|
||||||
|
throw new LedgerConcurrencyError(update.expected_version, this.version)
|
||||||
|
}
|
||||||
|
this.checkCascade(update)
|
||||||
|
|
||||||
|
const { expected_version: _ignored, ...rest } = update
|
||||||
|
const entry: PositionEntry = {
|
||||||
|
...rest,
|
||||||
|
recorded_at: this.cfg.clock?.() ?? new Date().toISOString(),
|
||||||
|
}
|
||||||
|
this.entries.push(entry)
|
||||||
|
this.version += 1
|
||||||
|
return this.read()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constraint cascade (P7): a DAY_AHEAD bid submission must stay within the
|
||||||
|
* band around the pro-rata daily share of the MONTHLY contracted position.
|
||||||
|
* No monthly position for the month → nothing to cascade from → reject
|
||||||
|
* (bidding without a position of record is a lineage failure, not a default-allow).
|
||||||
|
*/
|
||||||
|
private checkCascade(update: PositionUpdate): void {
|
||||||
|
if (update.kind !== 'BID_SUBMITTED' || update.timescale !== 'DAY_AHEAD') return
|
||||||
|
|
||||||
|
const month = update.period.slice(0, 7)
|
||||||
|
const monthly = this.entries.filter(
|
||||||
|
(e) => e.timescale === 'MONTHLY' && e.kind === 'CONTRACT' && e.period === month,
|
||||||
|
)
|
||||||
|
if (monthly.length === 0) {
|
||||||
|
throw new CascadeViolation(
|
||||||
|
`no MONTHLY CONTRACT position recorded for ${month}; day-ahead bid has no cascade anchor`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const contracted = monthly.reduce((sum, e) => sum.add(new Decimal(e.energy_mwh)), new Decimal(0))
|
||||||
|
const daysInMonth = new Date(
|
||||||
|
Date.UTC(Number(month.slice(0, 4)), Number(month.slice(5, 7)), 0),
|
||||||
|
).getUTCDate()
|
||||||
|
const dailyShare = contracted.div(daysInMonth)
|
||||||
|
const band = new Decimal(this.cfg.daMonthlyDeviationBand)
|
||||||
|
const min = dailyShare.mul(new Decimal(1).sub(band))
|
||||||
|
const max = dailyShare.mul(new Decimal(1).add(band))
|
||||||
|
const bid = new Decimal(update.energy_mwh)
|
||||||
|
|
||||||
|
if (bid.lt(min) || bid.gt(max)) {
|
||||||
|
throw new CascadeViolation(
|
||||||
|
`day-ahead bid ${bid.toString()} MWh outside monthly cascade band ` +
|
||||||
|
`[${min.toString()}, ${max.toString()}] (contracted ${contracted.toString()} MWh / ${daysInMonth} days ± ${band.mul(100).toString()}%)`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
packages/services/src/quality.ts
Normal file
20
packages/services/src/quality.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import type { Curve96 } from '@vpp/domain'
|
||||||
|
|
||||||
|
export interface QualityVerdict {
|
||||||
|
ok: boolean
|
||||||
|
issues: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data-quality gate stub (docs/05 §3.2). Full checks (missing-rate thresholds,
|
||||||
|
* jump detection, timestamp alignment, drift) arrive with the ingestion
|
||||||
|
* pipeline in M1 follow-up work; the interface is fixed now so callers can
|
||||||
|
* depend on it. Quality flags propagate through lineage (docs/13 §5).
|
||||||
|
*/
|
||||||
|
export function qualityGate(curve: Curve96): QualityVerdict {
|
||||||
|
const issues: string[] = []
|
||||||
|
if (new Set(curve.values).size === 1 && curve.values[0] === '0') {
|
||||||
|
issues.push('flat-zero curve: likely telemetry outage, not real data')
|
||||||
|
}
|
||||||
|
return { ok: issues.length === 0, issues }
|
||||||
|
}
|
||||||
85
packages/services/src/snapshot.ts
Normal file
85
packages/services/src/snapshot.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { createHash } from 'node:crypto'
|
||||||
|
import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs'
|
||||||
|
import { join } from 'node:path'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Content-addressed immutable snapshot store (invariant I7: decisions must be
|
||||||
|
* replayable from evidence). The ref IS the sha256 of canonical content, so
|
||||||
|
* a stored snapshot can never change under its ref by construction.
|
||||||
|
*/
|
||||||
|
export interface SnapshotStore {
|
||||||
|
put(value: unknown): string
|
||||||
|
get(ref: string): unknown | undefined
|
||||||
|
has(ref: string): boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Deterministic JSON: recursively sorted object keys, no whitespace variance. */
|
||||||
|
export const canonicalJson = (value: unknown): string =>
|
||||||
|
JSON.stringify(sortKeys(value))
|
||||||
|
|
||||||
|
const sortKeys = (v: unknown): unknown => {
|
||||||
|
if (Array.isArray(v)) return v.map(sortKeys)
|
||||||
|
if (v !== null && typeof v === 'object') {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(v as Record<string, unknown>)
|
||||||
|
.sort(([a], [b]) => (a < b ? -1 : 1))
|
||||||
|
.map(([k, val]) => [k, sortKeys(val)]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (typeof v === 'number' && !Number.isInteger(v)) {
|
||||||
|
// Floats in snapshots would make canonicalization platform-dependent and
|
||||||
|
// violate the decimal-string rule upstream (docs/11 §3.3).
|
||||||
|
throw new Error('non-integer number in snapshot content — use decimal strings')
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
export const contentRef = (canonical: string): string =>
|
||||||
|
createHash('sha256').update(canonical, 'utf8').digest('hex')
|
||||||
|
|
||||||
|
export class MemorySnapshotStore implements SnapshotStore {
|
||||||
|
private readonly byRef = new Map<string, string>()
|
||||||
|
|
||||||
|
put(value: unknown): string {
|
||||||
|
const canonical = canonicalJson(value)
|
||||||
|
const ref = contentRef(canonical)
|
||||||
|
if (!this.byRef.has(ref)) this.byRef.set(ref, canonical)
|
||||||
|
return ref
|
||||||
|
}
|
||||||
|
|
||||||
|
get(ref: string): unknown | undefined {
|
||||||
|
const canonical = this.byRef.get(ref)
|
||||||
|
return canonical === undefined ? undefined : JSON.parse(canonical)
|
||||||
|
}
|
||||||
|
|
||||||
|
has(ref: string): boolean {
|
||||||
|
return this.byRef.has(ref)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Filesystem adapter: one file per ref, write-once. */
|
||||||
|
export class FsSnapshotStore implements SnapshotStore {
|
||||||
|
constructor(private readonly dir: string) {
|
||||||
|
mkdirSync(dir, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
private path(ref: string): string {
|
||||||
|
return join(this.dir, `${ref}.json`)
|
||||||
|
}
|
||||||
|
|
||||||
|
put(value: unknown): string {
|
||||||
|
const canonical = canonicalJson(value)
|
||||||
|
const ref = contentRef(canonical)
|
||||||
|
if (!existsSync(this.path(ref))) writeFileSync(this.path(ref), canonical, { flag: 'wx' })
|
||||||
|
return ref
|
||||||
|
}
|
||||||
|
|
||||||
|
get(ref: string): unknown | undefined {
|
||||||
|
if (!this.has(ref)) return undefined
|
||||||
|
return JSON.parse(readFileSync(this.path(ref), 'utf8'))
|
||||||
|
}
|
||||||
|
|
||||||
|
has(ref: string): boolean {
|
||||||
|
return existsSync(this.path(ref))
|
||||||
|
}
|
||||||
|
}
|
||||||
77
packages/services/test/ledger.test.ts
Normal file
77
packages/services/test/ledger.test.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import type { PositionUpdate } from '@vpp/domain'
|
||||||
|
import { CascadeViolation, LedgerConcurrencyError, LedgerService } from '../src/ledger.js'
|
||||||
|
|
||||||
|
const clock = () => '2026-03-14T08:00:00Z'
|
||||||
|
|
||||||
|
const monthlyContract: PositionUpdate = {
|
||||||
|
id: 'pos-monthly',
|
||||||
|
timescale: 'MONTHLY',
|
||||||
|
period: '2026-03',
|
||||||
|
kind: 'CONTRACT',
|
||||||
|
energy_mwh: '37200.0', // 1200 MWh/day over 31 days
|
||||||
|
curve: null,
|
||||||
|
source_ref: 'contract-2026-03-001',
|
||||||
|
expected_version: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
const daBid = (energy: string, expectedVersion: number): PositionUpdate => ({
|
||||||
|
id: `pos-da-${energy}`,
|
||||||
|
timescale: 'DAY_AHEAD',
|
||||||
|
period: '2026-03-15',
|
||||||
|
kind: 'BID_SUBMITTED',
|
||||||
|
energy_mwh: energy,
|
||||||
|
curve: null,
|
||||||
|
source_ref: 'b'.repeat(64),
|
||||||
|
expected_version: expectedVersion,
|
||||||
|
})
|
||||||
|
|
||||||
|
const makeLedger = () => new LedgerService({ daMonthlyDeviationBand: '0.05', clock })
|
||||||
|
|
||||||
|
describe('constraint cascade (P7): monthly position bounds day-ahead bids', () => {
|
||||||
|
it('accepts a bid within the ±5% band of the pro-rata daily share', () => {
|
||||||
|
const ledger = makeLedger()
|
||||||
|
ledger.append(monthlyContract)
|
||||||
|
const view = ledger.append(daBid('1240.0', 1)) // daily share 1200, band [1140, 1260]
|
||||||
|
expect(view.version).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects a bid above the band', () => {
|
||||||
|
const ledger = makeLedger()
|
||||||
|
ledger.append(monthlyContract)
|
||||||
|
expect(() => ledger.append(daBid('1300.0', 1))).toThrow(CascadeViolation)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects a bid below the band', () => {
|
||||||
|
const ledger = makeLedger()
|
||||||
|
ledger.append(monthlyContract)
|
||||||
|
expect(() => ledger.append(daBid('1000.0', 1))).toThrow(CascadeViolation)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects a day-ahead bid with no monthly anchor (no default-allow)', () => {
|
||||||
|
const ledger = makeLedger()
|
||||||
|
expect(() => ledger.append(daBid('1200.0', 0))).toThrow(CascadeViolation)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('optimistic concurrency', () => {
|
||||||
|
it('rejects an append against a stale version', () => {
|
||||||
|
const ledger = makeLedger()
|
||||||
|
ledger.append(monthlyContract)
|
||||||
|
expect(() => ledger.append({ ...monthlyContract, id: 'pos-2', expected_version: 0 })).toThrow(
|
||||||
|
LedgerConcurrencyError,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('views', () => {
|
||||||
|
it('filters by timescale and stamps recorded_at from the injected clock', () => {
|
||||||
|
const ledger = makeLedger()
|
||||||
|
ledger.append(monthlyContract)
|
||||||
|
ledger.append(daBid('1200.0', 1))
|
||||||
|
const monthly = ledger.viewByTimescale('MONTHLY')
|
||||||
|
expect(monthly).toHaveLength(1)
|
||||||
|
expect(monthly[0]!.recorded_at).toBe(clock())
|
||||||
|
expect(ledger.viewByTimescale('DAY_AHEAD')).toHaveLength(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
39
packages/services/test/snapshot.test.ts
Normal file
39
packages/services/test/snapshot.test.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { mkdtempSync } from 'node:fs'
|
||||||
|
import { tmpdir } from 'node:os'
|
||||||
|
import { join } from 'node:path'
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { FsSnapshotStore, MemorySnapshotStore, canonicalJson, contentRef } from '../src/snapshot.js'
|
||||||
|
|
||||||
|
describe.each([
|
||||||
|
['memory', () => new MemorySnapshotStore()],
|
||||||
|
['fs', () => new FsSnapshotStore(mkdtempSync(join(tmpdir(), 'vpp-snap-')))],
|
||||||
|
])('snapshot store (%s)', (_label, make) => {
|
||||||
|
it('is content-addressed: same content → same ref, regardless of key order', () => {
|
||||||
|
const store = make()
|
||||||
|
const ref1 = store.put({ b: '2', a: '1' })
|
||||||
|
const ref2 = store.put({ a: '1', b: '2' })
|
||||||
|
expect(ref1).toBe(ref2)
|
||||||
|
expect(ref1).toMatch(/^[0-9a-f]{64}$/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('round-trips content and reports presence', () => {
|
||||||
|
const store = make()
|
||||||
|
const value = { curve: ['1.0', '2.5'], meta: { unit: 'mw' } }
|
||||||
|
const ref = store.put(value)
|
||||||
|
expect(store.has(ref)).toBe(true)
|
||||||
|
expect(store.get(ref)).toEqual(value)
|
||||||
|
expect(store.get('f'.repeat(64))).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ref verifies against canonical content (checksummed by construction)', () => {
|
||||||
|
const store = make()
|
||||||
|
const value = { x: '42.0' }
|
||||||
|
const ref = store.put(value)
|
||||||
|
expect(contentRef(canonicalJson(store.get(ref)))).toBe(ref)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects float content (decimal-string rule, docs/11 §3.3)', () => {
|
||||||
|
const store = make()
|
||||||
|
expect(() => store.put({ price: 425.5 })).toThrow(/decimal strings/)
|
||||||
|
})
|
||||||
|
})
|
||||||
8
packages/services/tsconfig.json
Normal file
8
packages/services/tsconfig.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": ".",
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"include": ["src", "test"]
|
||||||
|
}
|
||||||
3
skills-py/requirements.txt
Normal file
3
skills-py/requirements.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pydantic>=2.7
|
||||||
|
datamodel-code-generator>=0.26
|
||||||
|
pytest>=8.0
|
||||||
16
skills-py/scripts/generate_models.sh
Executable file
16
skills-py/scripts/generate_models.sh
Executable file
@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Regenerates skills-py/vpp_contracts from contracts/schema (ADR-0002).
|
||||||
|
# Generated files are committed but NEVER hand-edited.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
datamodel-codegen \
|
||||||
|
--input ../contracts/schema \
|
||||||
|
--input-file-type jsonschema \
|
||||||
|
--output vpp_contracts \
|
||||||
|
--output-model-type pydantic_v2.BaseModel \
|
||||||
|
--target-python-version 3.11 \
|
||||||
|
--use-standard-collections \
|
||||||
|
--disable-timestamp
|
||||||
|
|
||||||
|
echo "models regenerated in skills-py/vpp_contracts"
|
||||||
37
skills-py/tests/test_fixtures.py
Normal file
37
skills-py/tests/test_fixtures.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
"""Dual-side contract test (docs/11 §3.2): the same golden fixtures the TS side
|
||||||
|
validates must validate against the generated pydantic models, and the same
|
||||||
|
invalid fixtures must be rejected. Disagreement between sides fails CI."""
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from pydantic import BaseModel, ValidationError
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2] / "contracts"
|
||||||
|
|
||||||
|
|
||||||
|
def model_for(name: str) -> type[BaseModel]:
|
||||||
|
module = importlib.import_module(f"vpp_contracts.{name}")
|
||||||
|
class_name = "".join(part.capitalize() for part in name.split("_"))
|
||||||
|
return getattr(module, class_name)
|
||||||
|
|
||||||
|
|
||||||
|
def cases(dirname: str) -> list[tuple[str, Path]]:
|
||||||
|
out = []
|
||||||
|
for schema_dir in sorted((ROOT / dirname).iterdir()):
|
||||||
|
for f in sorted(schema_dir.glob("*.json")):
|
||||||
|
out.append((schema_dir.name, f))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("name,path", cases("fixtures"), ids=lambda p: str(p))
|
||||||
|
def test_golden_fixture_validates(name: str, path: Path) -> None:
|
||||||
|
# JSON-mode validation — exactly how a skill service receives these objects.
|
||||||
|
model_for(name).model_validate_json(path.read_text())
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("name,path", cases("fixtures-invalid"), ids=lambda p: str(p))
|
||||||
|
def test_invalid_fixture_rejected(name: str, path: Path) -> None:
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
model_for(name).model_validate_json(path.read_text())
|
||||||
2
skills-py/vpp_contracts/__init__.py
Normal file
2
skills-py/vpp_contracts/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# generated by datamodel-codegen:
|
||||||
|
# filename: schema
|
||||||
68
skills-py/vpp_contracts/approval.py
Normal file
68
skills-py/vpp_contracts/approval.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# generated by datamodel-codegen:
|
||||||
|
# filename: approval.json
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import StrEnum
|
||||||
|
|
||||||
|
from pydantic import AwareDatetime, BaseModel, ConfigDict, Field, conint, constr
|
||||||
|
|
||||||
|
|
||||||
|
class Decision(StrEnum):
|
||||||
|
APPROVE = 'APPROVE'
|
||||||
|
REJECT = 'REJECT'
|
||||||
|
|
||||||
|
|
||||||
|
class Approver(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
id: constr(min_length=1)
|
||||||
|
role: constr(min_length=1)
|
||||||
|
|
||||||
|
|
||||||
|
class EffectType(StrEnum):
|
||||||
|
BID = 'BID'
|
||||||
|
INVITATION = 'INVITATION'
|
||||||
|
CONTROL_PLAN = 'CONTROL_PLAN'
|
||||||
|
DISPATCH_PLAN = 'DISPATCH_PLAN'
|
||||||
|
|
||||||
|
|
||||||
|
class Scope(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
effect_type: EffectType
|
||||||
|
limits: dict[str, constr(pattern=r'^-?\d+(\.\d+)?$')]
|
||||||
|
|
||||||
|
|
||||||
|
class Validity(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
from_: AwareDatetime = Field(..., alias='from')
|
||||||
|
to: AwareDatetime
|
||||||
|
|
||||||
|
|
||||||
|
class EvidenceVersions(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
policy_pack_version: constr(min_length=1)
|
||||||
|
ledger_version: conint(ge=0, le=9007199254740991)
|
||||||
|
data_snapshot_refs: list[constr(pattern=r'^[0-9a-f]{64}$')]
|
||||||
|
|
||||||
|
|
||||||
|
class Approval(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
id: constr(min_length=1)
|
||||||
|
proposal_digest: constr(pattern=r'^[0-9a-f]{64}$')
|
||||||
|
decision: Decision
|
||||||
|
approver: Approver
|
||||||
|
scope: Scope
|
||||||
|
validity: Validity
|
||||||
|
evidence_versions: EvidenceVersions
|
||||||
|
comment: str | None = None
|
||||||
|
decided_at: AwareDatetime
|
||||||
51
skills-py/vpp_contracts/decision_case.py
Normal file
51
skills-py/vpp_contracts/decision_case.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# generated by datamodel-codegen:
|
||||||
|
# filename: decision_case.json
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import StrEnum
|
||||||
|
|
||||||
|
from pydantic import AwareDatetime, BaseModel, ConfigDict, constr
|
||||||
|
|
||||||
|
|
||||||
|
class Kind(StrEnum):
|
||||||
|
DAY_AHEAD_BID = 'DAY_AHEAD_BID'
|
||||||
|
DR_EVENT = 'DR_EVENT'
|
||||||
|
DEVIATION_RESOLUTION = 'DEVIATION_RESOLUTION'
|
||||||
|
REVIEW = 'REVIEW'
|
||||||
|
ADHOC_ANALYSIS = 'ADHOC_ANALYSIS'
|
||||||
|
|
||||||
|
|
||||||
|
class Status(StrEnum):
|
||||||
|
OPEN = 'OPEN'
|
||||||
|
IN_PROGRESS = 'IN_PROGRESS'
|
||||||
|
AWAITING_APPROVAL = 'AWAITING_APPROVAL'
|
||||||
|
CLOSED_DONE = 'CLOSED_DONE'
|
||||||
|
CLOSED_ABORTED = 'CLOSED_ABORTED'
|
||||||
|
|
||||||
|
|
||||||
|
class Refs(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
evidence: list[constr(min_length=1)]
|
||||||
|
proposals: list[constr(min_length=1)]
|
||||||
|
approvals: list[constr(min_length=1)]
|
||||||
|
permits: list[constr(min_length=1)]
|
||||||
|
scenarios: list[constr(min_length=1)]
|
||||||
|
outcome: constr(min_length=1) | None
|
||||||
|
|
||||||
|
|
||||||
|
class DecisionCase(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
id: constr(min_length=1)
|
||||||
|
kind: Kind
|
||||||
|
objective: constr(min_length=1)
|
||||||
|
owner: constr(min_length=1)
|
||||||
|
deadline: AwareDatetime
|
||||||
|
status: Status
|
||||||
|
refs: Refs
|
||||||
|
opened_at: AwareDatetime
|
||||||
|
closed_at: AwareDatetime | None
|
||||||
93
skills-py/vpp_contracts/envelope.py
Normal file
93
skills-py/vpp_contracts/envelope.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
# generated by datamodel-codegen:
|
||||||
|
# filename: envelope.json
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import StrEnum
|
||||||
|
|
||||||
|
from pydantic import (
|
||||||
|
AwareDatetime,
|
||||||
|
BaseModel,
|
||||||
|
ConfigDict,
|
||||||
|
Field,
|
||||||
|
RootModel,
|
||||||
|
conint,
|
||||||
|
constr,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProposalType(StrEnum):
|
||||||
|
BID = 'BID'
|
||||||
|
INVITATION = 'INVITATION'
|
||||||
|
CONTROL_PLAN = 'CONTROL_PLAN'
|
||||||
|
DISPATCH_PLAN = 'DISPATCH_PLAN'
|
||||||
|
|
||||||
|
|
||||||
|
class Timescale(StrEnum):
|
||||||
|
ANNUAL = 'ANNUAL'
|
||||||
|
MONTHLY = 'MONTHLY'
|
||||||
|
DAY_AHEAD = 'DAY_AHEAD'
|
||||||
|
INTRADAY = 'INTRADAY'
|
||||||
|
REALTIME = 'REALTIME'
|
||||||
|
|
||||||
|
|
||||||
|
class Scope(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
proposal_type: ProposalType
|
||||||
|
timescales: list[Timescale] = Field(..., min_length=1)
|
||||||
|
resource_set: constr(min_length=1)
|
||||||
|
|
||||||
|
|
||||||
|
class Validity(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
from_: AwareDatetime = Field(..., alias='from')
|
||||||
|
to: AwareDatetime
|
||||||
|
|
||||||
|
|
||||||
|
class Level(StrEnum):
|
||||||
|
L1 = 'L1'
|
||||||
|
L2 = 'L2'
|
||||||
|
L3 = 'L3'
|
||||||
|
|
||||||
|
|
||||||
|
class ApprovedByItem(RootModel[constr(min_length=1)]):
|
||||||
|
root: constr(min_length=1)
|
||||||
|
|
||||||
|
|
||||||
|
class Approval(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
level: Level
|
||||||
|
approved_by: list[ApprovedByItem] = Field(..., min_length=1)
|
||||||
|
|
||||||
|
|
||||||
|
class Escalation(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
max_consecutive_deviations: conint(le=9007199254740991, gt=0)
|
||||||
|
deviation_threshold_pct: constr(pattern=r'^-?\d+(\.\d+)?$')
|
||||||
|
|
||||||
|
|
||||||
|
class Status(StrEnum):
|
||||||
|
ACTIVE = 'ACTIVE'
|
||||||
|
SUSPENDED = 'SUSPENDED'
|
||||||
|
EXPIRED = 'EXPIRED'
|
||||||
|
|
||||||
|
|
||||||
|
class Envelope(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
id: constr(min_length=1)
|
||||||
|
scope: Scope
|
||||||
|
bounds: dict[str, constr(pattern=r'^-?\d+(\.\d+)?$')]
|
||||||
|
validity: Validity
|
||||||
|
approval: Approval
|
||||||
|
escalation: Escalation
|
||||||
|
status: Status
|
||||||
21
skills-py/vpp_contracts/event_envelope.py
Normal file
21
skills-py/vpp_contracts/event_envelope.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# generated by datamodel-codegen:
|
||||||
|
# filename: event_envelope.json
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pydantic import AwareDatetime, BaseModel, ConfigDict, constr
|
||||||
|
|
||||||
|
|
||||||
|
class EventEnvelope(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
event_id: constr(min_length=1)
|
||||||
|
event_type: constr(min_length=1)
|
||||||
|
schema_version: constr(pattern=r'^\d+\.\d+\.\d+$')
|
||||||
|
occurred_at: AwareDatetime
|
||||||
|
causation_id: constr(min_length=1)
|
||||||
|
correlation_id: constr(min_length=1)
|
||||||
|
payload: Any
|
||||||
21
skills-py/vpp_contracts/execution_permit.py
Normal file
21
skills-py/vpp_contracts/execution_permit.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# generated by datamodel-codegen:
|
||||||
|
# filename: execution_permit.json
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from pydantic import AwareDatetime, BaseModel, ConfigDict, constr
|
||||||
|
|
||||||
|
|
||||||
|
class ExecutionPermit(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
id: constr(min_length=1)
|
||||||
|
proposal_digest: constr(pattern=r'^[0-9a-f]{64}$')
|
||||||
|
issued_at: AwareDatetime
|
||||||
|
expires_at: AwareDatetime
|
||||||
|
effect_limits: dict[str, constr(pattern=r'^-?\d+(\.\d+)?$')]
|
||||||
|
revoked_at: AwareDatetime | None
|
||||||
|
issuer: Literal['authority-service']
|
||||||
59
skills-py/vpp_contracts/flexibility_envelope.py
Normal file
59
skills-py/vpp_contracts/flexibility_envelope.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# generated by datamodel-codegen:
|
||||||
|
# filename: flexibility_envelope.json
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import StrEnum
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from pydantic import AwareDatetime, BaseModel, ConfigDict, Field, RootModel, constr
|
||||||
|
|
||||||
|
|
||||||
|
class Level(StrEnum):
|
||||||
|
SITE = 'SITE'
|
||||||
|
AGGREGATION_UNIT = 'AGGREGATION_UNIT'
|
||||||
|
PROVINCE = 'PROVINCE'
|
||||||
|
|
||||||
|
|
||||||
|
class Provider(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
unit_id: constr(min_length=1)
|
||||||
|
level: Level
|
||||||
|
|
||||||
|
|
||||||
|
class Value(RootModel[constr(pattern=r'^-?\d+(\.\d+)?$')]):
|
||||||
|
root: constr(pattern=r'^-?\d+(\.\d+)?$')
|
||||||
|
|
||||||
|
|
||||||
|
class UpMw(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
interval_minutes: Literal[15]
|
||||||
|
date: constr(pattern=r'^\d{4}-\d{2}-\d{2}$')
|
||||||
|
values: list[Value] = Field(..., max_length=96, min_length=96)
|
||||||
|
|
||||||
|
|
||||||
|
class DownMw(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
interval_minutes: Literal[15]
|
||||||
|
date: constr(pattern=r'^\d{4}-\d{2}-\d{2}$')
|
||||||
|
values: list[Value] = Field(..., max_length=96, min_length=96)
|
||||||
|
|
||||||
|
|
||||||
|
class FlexibilityEnvelope(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
id: constr(min_length=1)
|
||||||
|
provider: Provider
|
||||||
|
market_date: constr(pattern=r'^\d{4}-\d{2}-\d{2}$')
|
||||||
|
up_mw: UpMw
|
||||||
|
down_mw: DownMw
|
||||||
|
valid_until: AwareDatetime
|
||||||
|
schema_version: constr(min_length=1)
|
||||||
|
signature: str | None
|
||||||
82
skills-py/vpp_contracts/forecast_bundle.py
Normal file
82
skills-py/vpp_contracts/forecast_bundle.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
# generated by datamodel-codegen:
|
||||||
|
# filename: forecast_bundle.json
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import StrEnum
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from pydantic import AwareDatetime, BaseModel, ConfigDict, Field, RootModel, constr
|
||||||
|
|
||||||
|
|
||||||
|
class Kind(StrEnum):
|
||||||
|
LOAD = 'LOAD'
|
||||||
|
PV = 'PV'
|
||||||
|
PRICE = 'PRICE'
|
||||||
|
|
||||||
|
|
||||||
|
class Unit(StrEnum):
|
||||||
|
mw = 'mw'
|
||||||
|
yuan_per_mwh = 'yuan_per_mwh'
|
||||||
|
|
||||||
|
|
||||||
|
class Value(RootModel[constr(pattern=r'^-?\d+(\.\d+)?$')]):
|
||||||
|
root: constr(pattern=r'^-?\d+(\.\d+)?$')
|
||||||
|
|
||||||
|
|
||||||
|
class P10(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
interval_minutes: Literal[15]
|
||||||
|
date: constr(pattern=r'^\d{4}-\d{2}-\d{2}$')
|
||||||
|
values: list[Value] = Field(..., max_length=96, min_length=96)
|
||||||
|
|
||||||
|
|
||||||
|
class P50(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
interval_minutes: Literal[15]
|
||||||
|
date: constr(pattern=r'^\d{4}-\d{2}-\d{2}$')
|
||||||
|
values: list[Value] = Field(..., max_length=96, min_length=96)
|
||||||
|
|
||||||
|
|
||||||
|
class P90(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
interval_minutes: Literal[15]
|
||||||
|
date: constr(pattern=r'^\d{4}-\d{2}-\d{2}$')
|
||||||
|
values: list[Value] = Field(..., max_length=96, min_length=96)
|
||||||
|
|
||||||
|
|
||||||
|
class Quantiles(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
p10: P10
|
||||||
|
p50: P50
|
||||||
|
p90: P90
|
||||||
|
|
||||||
|
|
||||||
|
class Model(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
name: constr(min_length=1)
|
||||||
|
version: constr(min_length=1)
|
||||||
|
|
||||||
|
|
||||||
|
class ForecastBundle(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
id: constr(min_length=1)
|
||||||
|
kind: Kind
|
||||||
|
market_date: constr(pattern=r'^\d{4}-\d{2}-\d{2}$')
|
||||||
|
unit: Unit
|
||||||
|
quantiles: Quantiles
|
||||||
|
model: Model
|
||||||
|
features_snapshot_ref: constr(pattern=r'^[0-9a-f]{64}$')
|
||||||
|
generated_at: AwareDatetime
|
||||||
67
skills-py/vpp_contracts/ledger_view.py
Normal file
67
skills-py/vpp_contracts/ledger_view.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# generated by datamodel-codegen:
|
||||||
|
# filename: ledger_view.json
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import StrEnum
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from pydantic import (
|
||||||
|
AwareDatetime,
|
||||||
|
BaseModel,
|
||||||
|
ConfigDict,
|
||||||
|
Field,
|
||||||
|
RootModel,
|
||||||
|
conint,
|
||||||
|
constr,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Timescale(StrEnum):
|
||||||
|
ANNUAL = 'ANNUAL'
|
||||||
|
MONTHLY = 'MONTHLY'
|
||||||
|
DAY_AHEAD = 'DAY_AHEAD'
|
||||||
|
INTRADAY = 'INTRADAY'
|
||||||
|
REALTIME = 'REALTIME'
|
||||||
|
|
||||||
|
|
||||||
|
class Kind(StrEnum):
|
||||||
|
CONTRACT = 'CONTRACT'
|
||||||
|
BID_SUBMITTED = 'BID_SUBMITTED'
|
||||||
|
AWARD = 'AWARD'
|
||||||
|
EXTERNAL_COMMITMENT = 'EXTERNAL_COMMITMENT'
|
||||||
|
|
||||||
|
|
||||||
|
class Value(RootModel[constr(pattern=r'^-?\d+(\.\d+)?$')]):
|
||||||
|
root: constr(pattern=r'^-?\d+(\.\d+)?$')
|
||||||
|
|
||||||
|
|
||||||
|
class Curve(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
interval_minutes: Literal[15]
|
||||||
|
date: constr(pattern=r'^\d{4}-\d{2}-\d{2}$')
|
||||||
|
values: list[Value] = Field(..., max_length=96, min_length=96)
|
||||||
|
|
||||||
|
|
||||||
|
class Entry(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
id: constr(min_length=1)
|
||||||
|
timescale: Timescale
|
||||||
|
period: constr(pattern=r'^\d{4}(-\d{2}(-\d{2})?)?$')
|
||||||
|
kind: Kind
|
||||||
|
energy_mwh: constr(pattern=r'^-?\d+(\.\d+)?$')
|
||||||
|
curve: Curve | None
|
||||||
|
source_ref: constr(min_length=1)
|
||||||
|
recorded_at: AwareDatetime
|
||||||
|
|
||||||
|
|
||||||
|
class LedgerView(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
version: conint(ge=0, le=9007199254740991)
|
||||||
|
entries: list[Entry]
|
||||||
51
skills-py/vpp_contracts/position_update.py
Normal file
51
skills-py/vpp_contracts/position_update.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# generated by datamodel-codegen:
|
||||||
|
# filename: position_update.json
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import StrEnum
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field, RootModel, conint, constr
|
||||||
|
|
||||||
|
|
||||||
|
class Timescale(StrEnum):
|
||||||
|
ANNUAL = 'ANNUAL'
|
||||||
|
MONTHLY = 'MONTHLY'
|
||||||
|
DAY_AHEAD = 'DAY_AHEAD'
|
||||||
|
INTRADAY = 'INTRADAY'
|
||||||
|
REALTIME = 'REALTIME'
|
||||||
|
|
||||||
|
|
||||||
|
class Kind(StrEnum):
|
||||||
|
CONTRACT = 'CONTRACT'
|
||||||
|
BID_SUBMITTED = 'BID_SUBMITTED'
|
||||||
|
AWARD = 'AWARD'
|
||||||
|
EXTERNAL_COMMITMENT = 'EXTERNAL_COMMITMENT'
|
||||||
|
|
||||||
|
|
||||||
|
class Value(RootModel[constr(pattern=r'^-?\d+(\.\d+)?$')]):
|
||||||
|
root: constr(pattern=r'^-?\d+(\.\d+)?$')
|
||||||
|
|
||||||
|
|
||||||
|
class Curve(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
interval_minutes: Literal[15]
|
||||||
|
date: constr(pattern=r'^\d{4}-\d{2}-\d{2}$')
|
||||||
|
values: list[Value] = Field(..., max_length=96, min_length=96)
|
||||||
|
|
||||||
|
|
||||||
|
class PositionUpdate(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
id: constr(min_length=1)
|
||||||
|
timescale: Timescale
|
||||||
|
period: constr(pattern=r'^\d{4}(-\d{2}(-\d{2})?)?$')
|
||||||
|
kind: Kind
|
||||||
|
energy_mwh: constr(pattern=r'^-?\d+(\.\d+)?$')
|
||||||
|
curve: Curve | None
|
||||||
|
source_ref: constr(min_length=1)
|
||||||
|
expected_version: conint(ge=0, le=9007199254740991)
|
||||||
132
skills-py/vpp_contracts/proposal.py
Normal file
132
skills-py/vpp_contracts/proposal.py
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
# generated by datamodel-codegen:
|
||||||
|
# filename: proposal.json
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import StrEnum
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from pydantic import (
|
||||||
|
AwareDatetime,
|
||||||
|
BaseModel,
|
||||||
|
ConfigDict,
|
||||||
|
Field,
|
||||||
|
RootModel,
|
||||||
|
conint,
|
||||||
|
constr,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Timescale(StrEnum):
|
||||||
|
ANNUAL = 'ANNUAL'
|
||||||
|
MONTHLY = 'MONTHLY'
|
||||||
|
DAY_AHEAD = 'DAY_AHEAD'
|
||||||
|
INTRADAY = 'INTRADAY'
|
||||||
|
REALTIME = 'REALTIME'
|
||||||
|
|
||||||
|
|
||||||
|
class Trigger(StrEnum):
|
||||||
|
MANUAL = 'MANUAL'
|
||||||
|
EVENT = 'EVENT'
|
||||||
|
SCHEDULED = 'SCHEDULED'
|
||||||
|
|
||||||
|
|
||||||
|
class Originator(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
agent: constr(min_length=1)
|
||||||
|
trigger: Trigger
|
||||||
|
task_id: constr(min_length=1)
|
||||||
|
|
||||||
|
|
||||||
|
class ToolCall(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
tool_call_id: constr(min_length=1)
|
||||||
|
tool: constr(min_length=1)
|
||||||
|
version: constr(min_length=1)
|
||||||
|
inputs_ref: constr(pattern=r'^[0-9a-f]{64}$')
|
||||||
|
outputs_ref: constr(pattern=r'^[0-9a-f]{64}$')
|
||||||
|
|
||||||
|
|
||||||
|
class Lineage(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
tool_calls: list[ToolCall]
|
||||||
|
data_refs: list[constr(pattern=r'^[0-9a-f]{64}$')]
|
||||||
|
ledger_version: conint(ge=0, le=9007199254740991)
|
||||||
|
policy_pack_version: constr(min_length=1)
|
||||||
|
|
||||||
|
|
||||||
|
class Status(StrEnum):
|
||||||
|
DRAFT = 'DRAFT'
|
||||||
|
RULE_CHECK = 'RULE_CHECK'
|
||||||
|
SIMULATION = 'SIMULATION'
|
||||||
|
ENVELOPE_CHECK = 'ENVELOPE_CHECK'
|
||||||
|
PENDING_HUMAN = 'PENDING_HUMAN'
|
||||||
|
AUTO_APPROVED = 'AUTO_APPROVED'
|
||||||
|
APPROVED = 'APPROVED'
|
||||||
|
FRESH_CHECK = 'FRESH_CHECK'
|
||||||
|
STALE = 'STALE'
|
||||||
|
AUTHORIZED = 'AUTHORIZED'
|
||||||
|
RELEASED = 'RELEASED'
|
||||||
|
EXECUTING = 'EXECUTING'
|
||||||
|
COMPLETED = 'COMPLETED'
|
||||||
|
ROLLED_BACK = 'ROLLED_BACK'
|
||||||
|
REJECTED = 'REJECTED'
|
||||||
|
|
||||||
|
|
||||||
|
class Value(RootModel[constr(pattern=r'^-?\d+(\.\d+)?$')]):
|
||||||
|
root: constr(pattern=r'^-?\d+(\.\d+)?$')
|
||||||
|
|
||||||
|
|
||||||
|
class PricesYuanPerMwh(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
interval_minutes: Literal[15]
|
||||||
|
date: constr(pattern=r'^\d{4}-\d{2}-\d{2}$')
|
||||||
|
values: list[Value] = Field(..., max_length=96, min_length=96)
|
||||||
|
|
||||||
|
|
||||||
|
class QuantitiesMwh(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
interval_minutes: Literal[15]
|
||||||
|
date: constr(pattern=r'^\d{4}-\d{2}-\d{2}$')
|
||||||
|
values: list[Value] = Field(..., max_length=96, min_length=96)
|
||||||
|
|
||||||
|
|
||||||
|
class Payload(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
kind: Literal['BID']
|
||||||
|
market_date: constr(pattern=r'^\d{4}-\d{2}-\d{2}$')
|
||||||
|
prices_yuan_per_mwh: PricesYuanPerMwh
|
||||||
|
quantities_mwh: QuantitiesMwh
|
||||||
|
expected_revenue_yuan: constr(pattern=r'^-?\d+(\.\d+)?$')
|
||||||
|
|
||||||
|
|
||||||
|
class Proposal1(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
id: constr(min_length=1)
|
||||||
|
timescale: Timescale
|
||||||
|
originator: Originator
|
||||||
|
lineage: Lineage
|
||||||
|
envelope_ref: constr(min_length=1) | None
|
||||||
|
digest: constr(pattern=r'^[0-9a-f]{64}$')
|
||||||
|
status: Status
|
||||||
|
created_at: AwareDatetime
|
||||||
|
type: Literal['BID']
|
||||||
|
payload: Payload
|
||||||
|
|
||||||
|
|
||||||
|
class Proposal(RootModel[Proposal1]):
|
||||||
|
root: Proposal1 = Field(..., title='Proposal')
|
||||||
42
skills-py/vpp_contracts/resource_profile.py
Normal file
42
skills-py/vpp_contracts/resource_profile.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# generated by datamodel-codegen:
|
||||||
|
# filename: resource_profile.json
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import StrEnum
|
||||||
|
|
||||||
|
from pydantic import AwareDatetime, BaseModel, ConfigDict, conint, constr
|
||||||
|
|
||||||
|
|
||||||
|
class Type(StrEnum):
|
||||||
|
STORAGE = 'STORAGE'
|
||||||
|
PV = 'PV'
|
||||||
|
EV_CHARGER = 'EV_CHARGER'
|
||||||
|
HVAC = 'HVAC'
|
||||||
|
INDUSTRIAL_LOAD = 'INDUSTRIAL_LOAD'
|
||||||
|
MICROGRID = 'MICROGRID'
|
||||||
|
BASE_STATION = 'BASE_STATION'
|
||||||
|
|
||||||
|
|
||||||
|
class Constraints(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
min_duration_min: conint(le=9007199254740991, gt=0) | None
|
||||||
|
recovery_rate_mw_per_min: constr(pattern=r'^-?\d+(\.\d+)?$') | None
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceProfile(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
resource_id: constr(min_length=1)
|
||||||
|
name: constr(min_length=1)
|
||||||
|
type: Type
|
||||||
|
rated_power_mw: constr(pattern=r'^-?\d+(\.\d+)?$')
|
||||||
|
certified_adjustable_mw: constr(pattern=r'^-?\d+(\.\d+)?$')
|
||||||
|
confidence: constr(pattern=r'^-?\d+(\.\d+)?$')
|
||||||
|
reliability_score: constr(pattern=r'^-?\d+(\.\d+)?$')
|
||||||
|
constraints: Constraints
|
||||||
|
evidence_refs: list[constr(pattern=r'^[0-9a-f]{64}$')]
|
||||||
|
updated_at: AwareDatetime
|
||||||
43
skills-py/vpp_contracts/situation_report.py
Normal file
43
skills-py/vpp_contracts/situation_report.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# generated by datamodel-codegen:
|
||||||
|
# filename: situation_report.json
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import StrEnum
|
||||||
|
|
||||||
|
from pydantic import AwareDatetime, BaseModel, ConfigDict, constr
|
||||||
|
|
||||||
|
|
||||||
|
class RiskLevel(StrEnum):
|
||||||
|
LOW = 'LOW'
|
||||||
|
MEDIUM = 'MEDIUM'
|
||||||
|
HIGH = 'HIGH'
|
||||||
|
EXTREME = 'EXTREME'
|
||||||
|
|
||||||
|
|
||||||
|
class Kind(StrEnum):
|
||||||
|
TREND = 'TREND'
|
||||||
|
ANOMALY = 'ANOMALY'
|
||||||
|
RISK = 'RISK'
|
||||||
|
ATTRIBUTION = 'ATTRIBUTION'
|
||||||
|
|
||||||
|
|
||||||
|
class Finding(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
kind: Kind
|
||||||
|
summary: constr(min_length=1)
|
||||||
|
refs: list[constr(pattern=r'^[0-9a-f]{64}$')]
|
||||||
|
|
||||||
|
|
||||||
|
class SituationReport(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
extra='forbid',
|
||||||
|
)
|
||||||
|
id: constr(min_length=1)
|
||||||
|
market_date: constr(pattern=r'^\d{4}-\d{2}-\d{2}$')
|
||||||
|
risk_level: RiskLevel
|
||||||
|
findings: list[Finding]
|
||||||
|
forecast_refs: list[constr(min_length=1)]
|
||||||
|
generated_at: AwareDatetime
|
||||||
15
tsconfig.base.json
Normal file
15
tsconfig.base.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"strict": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"exactOptionalPropertyTypes": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"declaration": true,
|
||||||
|
"verbatimModuleSyntax": true
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user