programgarden 2.6.0__tar.gz → 2.6.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. {programgarden-2.6.0 → programgarden-2.6.2}/PKG-INFO +8 -2
  2. {programgarden-2.6.0 → programgarden-2.6.2}/README.md +6 -0
  3. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/executor.py +50 -12
  4. programgarden-2.6.2/programgarden/futures_history.py +23 -0
  5. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/resolver.py +51 -0
  6. {programgarden-2.6.0 → programgarden-2.6.2}/pyproject.toml +2 -2
  7. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/__init__.py +0 -0
  8. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/binding_validator.py +0 -0
  9. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/client.py +0 -0
  10. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/code_worker.py +0 -0
  11. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/context.py +0 -0
  12. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/database/__init__.py +0 -0
  13. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/database/adjustment_delivery.py +0 -0
  14. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/database/broker_evidence.py +0 -0
  15. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/database/broker_order_totals.py +0 -0
  16. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/database/broker_snapshot.py +0 -0
  17. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/database/checkpoint_manager.py +0 -0
  18. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/database/db_naming.py +0 -0
  19. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/database/execution_storage.py +0 -0
  20. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/database/fill_reconciler.py +0 -0
  21. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/database/order_cancellation.py +0 -0
  22. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/database/order_recovery.py +0 -0
  23. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/database/owned_order_cancellation.py +0 -0
  24. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/database/position_reconciliation.py +0 -0
  25. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/database/query_builder.py +0 -0
  26. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/database/sqlite_scope.py +0 -0
  27. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/database/workflow_position_tracker.py +0 -0
  28. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/database/workflow_risk_tracker.py +0 -0
  29. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/deep_fixtures.py +0 -0
  30. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/futures_contract_info.py +0 -0
  31. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/futures_orderable.py +0 -0
  32. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/futures_pnl.py +0 -0
  33. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/futures_read_evidence.py +0 -0
  34. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/incremental_build.py +0 -0
  35. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/managed_order_control.py +0 -0
  36. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/node_runner.py +0 -0
  37. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/order_lifecycle.py +0 -0
  38. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/plugin/__init__.py +0 -0
  39. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/plugin/sandbox.py +0 -0
  40. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/providers/__init__.py +0 -0
  41. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/providers/llm_errors.py +0 -0
  42. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/providers/llm_provider.py +0 -0
  43. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/reconnect_handler.py +0 -0
  44. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/replay_contracts.py +0 -0
  45. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/replay_events.py +0 -0
  46. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/replay_external.py +0 -0
  47. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/replay_order_adapter.py +0 -0
  48. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/replay_orders.py +0 -0
  49. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/replay_scenarios.py +0 -0
  50. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/replay_semantics.py +0 -0
  51. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/replay_sources.py +0 -0
  52. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/replay_sqlite.py +0 -0
  53. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/replay_triggers.py +0 -0
  54. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/resource/__init__.py +0 -0
  55. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/resource/context.py +0 -0
  56. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/resource/limiter.py +0 -0
  57. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/resource/monitor.py +0 -0
  58. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/resource/throttle.py +0 -0
  59. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/semantic_rules.py +0 -0
  60. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/tools/__init__.py +0 -0
  61. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/tools/credential_tools.py +0 -0
  62. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/tools/definition_tools.py +0 -0
  63. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/tools/event_tools.py +0 -0
  64. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/tools/job_tools.py +0 -0
  65. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/tools/registry_tools.py +0 -0
  66. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/tools/sqlite_tools.py +0 -0
  67. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/validation_recommender.py +0 -0
  68. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/validation_replay.py +0 -0
  69. {programgarden-2.6.0 → programgarden-2.6.2}/programgarden/valuation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: programgarden
3
- Version: 2.6.0
3
+ Version: 2.6.2
4
4
  Summary: ProgramGarden - 노드 기반 자동매매 DSL 실행 엔진
5
5
  License-Expression: AGPL-3.0-or-later
6
6
  Author: 프로그램동산
@@ -16,7 +16,7 @@ Requires-Dist: croniter (>=6.0.0,<7.0.0)
16
16
  Requires-Dist: litellm (>=1.40.0)
17
17
  Requires-Dist: lxml (>=6.0.2,<7.0.0)
18
18
  Requires-Dist: programgarden-community (>=2.3.1,<3.0.0)
19
- Requires-Dist: programgarden-core (>=2.6.0,<3.0.0)
19
+ Requires-Dist: programgarden-core (>=2.6.2,<3.0.0)
20
20
  Requires-Dist: programgarden-finance (>=2.0.1,<3.0.0)
21
21
  Requires-Dist: psutil (>=6.0.0,<7.0.0)
22
22
  Requires-Dist: psycopg2-binary (>=2.9.11,<3.0.0)
@@ -27,8 +27,14 @@ Requires-Dist: tzdata (>=2025.2,<2026.0)
27
27
  Requires-Dist: yfinance (>=0.2.0,<0.3.0)
28
28
  Description-Content-Type: text/markdown
29
29
 
30
+ The 2.6.2 release candidate [futures minute alias correction](../../docs/futures-minute-alias-contract.md) aligns canonical minute periods with the existing one-page o3103 path and rejects unsupported hourly/adjustment claims. Complete-range coverage and consumer adoption are not implied.
31
+
30
32
  # ProgramGarden
31
33
 
34
+ An unreleased [stock history interval correction](../../docs/stock-history-interval-contract.md)
35
+ rejects unsupported intraday periods instead of silently querying daily bars.
36
+ Supported g3204 periods retain their existing requests and output contract.
37
+
32
38
  An unreleased [futures detail and durable state candidate](../../docs/futures-contract-details-and-rollover-state.md)
33
39
  adds exact o3105 metadata, opt-in execution-scoped SQLite and XHKG calendar
34
40
  intersection. Missing evidence remains explicit; this does not authorize orders.
@@ -1,5 +1,11 @@
1
+ The 2.6.2 release candidate [futures minute alias correction](../../docs/futures-minute-alias-contract.md) aligns canonical minute periods with the existing one-page o3103 path and rejects unsupported hourly/adjustment claims. Complete-range coverage and consumer adoption are not implied.
2
+
1
3
  # ProgramGarden
2
4
 
5
+ An unreleased [stock history interval correction](../../docs/stock-history-interval-contract.md)
6
+ rejects unsupported intraday periods instead of silently querying daily bars.
7
+ Supported g3204 periods retain their existing requests and output contract.
8
+
3
9
  An unreleased [futures detail and durable state candidate](../../docs/futures-contract-details-and-rollover-state.md)
4
10
  adds exact o3105 metadata, opt-in execution-scoped SQLite and XHKG calendar
5
11
  intersection. Missing evidence remains explicit; this does not authorize orders.
@@ -35,6 +35,12 @@ from programgarden_core import (
35
35
  OVERSEAS_STOCK_ORDER_MARKET_CODES,
36
36
  )
37
37
  from programgarden.context import ExecutionContext, WorkflowEvent
38
+ from programgarden.futures_history import (
39
+ DAILY_INTERVALS as FUTURES_DAILY_INTERVALS,
40
+ MINUTE_INTERVALS as FUTURES_MINUTE_INTERVALS,
41
+ normalize_interval as normalize_futures_history_interval,
42
+ validate_adjust as validate_futures_history_adjust,
43
+ )
38
44
  from programgarden.order_lifecycle import (
39
45
  FUTURES_PRODUCTS, OrderLifecycleMetadata, exact_futures_credential,
40
46
  inject_futures_connection, operation_key,
@@ -12186,7 +12192,7 @@ class HistoricalDataNodeExecutor(NodeExecutorBase):
12186
12192
  HistoricalDataNode executor - 과거 OHLCV 데이터 조회
12187
12193
 
12188
12194
  LS Finance Chart API를 사용하여 과거 차트 데이터를 조회합니다.
12189
- - 해외주식: g3103 (일/주/월봉)
12195
+ - Overseas stock: g3204 (daily/weekly/monthly/yearly; no intraday)
12190
12196
  - 해외선물: o3108 (일봉), o3103 (분봉)
12191
12197
  """
12192
12198
 
@@ -12208,6 +12214,20 @@ class HistoricalDataNodeExecutor(NodeExecutorBase):
12208
12214
  ) -> Dict[str, Any]:
12209
12215
  """과거 데이터 조회"""
12210
12216
 
12217
+ # Reject unsupported stock periods before fixtures can manufacture success.
12218
+ # The broker-specific fetch repeats this guard for direct/legacy callers.
12219
+ connection = config.get("connection")
12220
+ product = connection.get("product") if isinstance(connection, dict) else None
12221
+ if node_type == "OverseasStockHistoricalDataNode" or (
12222
+ node_type == "HistoricalDataNode" and product in (None, "overseas_stock")
12223
+ ):
12224
+ self._stock_history_gubun(config.get("interval", "1d"))
12225
+ if node_type == "OverseasFuturesHistoricalDataNode" or (
12226
+ node_type == "HistoricalDataNode" and product in FUTURES_PRODUCTS
12227
+ ):
12228
+ normalize_futures_history_interval(config.get("interval", "1d"))
12229
+ validate_futures_history_adjust(config.get("adjust", False))
12230
+
12211
12231
  # deep_validate: HistoricalDataNode hits the real LS API. Inject a fixture
12212
12232
  # time series so the flow completes without a network call.
12213
12233
  if context.is_deep_validate:
@@ -12451,6 +12471,18 @@ class HistoricalDataNodeExecutor(NodeExecutorBase):
12451
12471
  # 파싱 실패 시 오늘 날짜 반환
12452
12472
  return datetime.now().strftime("%Y%m%d")
12453
12473
 
12474
+ @staticmethod
12475
+ def _stock_history_gubun(interval: str) -> str:
12476
+ """Reject unsupported periods instead of relabeling daily bars."""
12477
+ periods = {"1d": "2", "1w": "3", "1M": "4", "1Y": "5"}
12478
+ if not isinstance(interval, str) or interval not in periods:
12479
+ raise ValueError(
12480
+ "UNSUPPORTED_STOCK_HISTORY_INTERVAL: OverseasStockHistoricalDataNode "
12481
+ "supports 1d, 1w, 1M and 1Y through LS g3204. "
12482
+ "Intraday g3203 is not supported; select a supported period explicitly."
12483
+ )
12484
+ return periods[interval]
12485
+
12454
12486
  async def _fetch_overseas_stock(
12455
12487
  self,
12456
12488
  symbols: List[str],
@@ -12472,6 +12504,9 @@ class HistoricalDataNodeExecutor(NodeExecutorBase):
12472
12504
  반환: [{symbol, exchange, time_series: [{date, open, high, low, close, volume}, ...]}, ...]
12473
12505
  """
12474
12506
 
12507
+ # Validate before credential access or login, outside the soft-failure handler.
12508
+ gubun = self._stock_history_gubun(interval)
12509
+
12475
12510
  credential = context.get_credential()
12476
12511
  if not credential:
12477
12512
  context.log("error", "Credential not set for HistoricalDataNode", node_id)
@@ -12494,10 +12529,6 @@ class HistoricalDataNodeExecutor(NodeExecutorBase):
12494
12529
 
12495
12530
  api = ls.overseas_stock()
12496
12531
 
12497
- # interval → gubun 변환 (g3204: 2=일, 3=주, 4=월, 5=년)
12498
- gubun_map = {"1d": "2", "1w": "3", "1M": "4", "1Y": "5"}
12499
- gubun = gubun_map.get(interval, "2")
12500
-
12501
12532
  # 거래소 코드 매핑 (역변환용)
12502
12533
  exchcd_to_exchange = {"82": "NASDAQ", "81": "NYSE", "83": "AMEX"}
12503
12534
 
@@ -12618,6 +12649,7 @@ class HistoricalDataNodeExecutor(NodeExecutorBase):
12618
12649
 
12619
12650
  반환: [{symbol, exchange, time_series: [{date, open, high, low, close, volume}, ...]}, ...]
12620
12651
  """
12652
+ interval = normalize_futures_history_interval(interval)
12621
12653
  credential = context.get_credential()
12622
12654
  if not credential:
12623
12655
  context.log("error", "Credential not set for HistoricalDataNode(futures)", node_id)
@@ -12649,7 +12681,7 @@ class HistoricalDataNodeExecutor(NodeExecutorBase):
12649
12681
  symbol_to_exchange[sym] = exch
12650
12682
 
12651
12683
  # interval별 분기
12652
- is_minute_data = interval in ["1min", "5min", "15min", "30min", "30s"]
12684
+ is_minute_data = interval in FUTURES_MINUTE_INTERVALS
12653
12685
 
12654
12686
  for symbol in symbols:
12655
12687
  try:
@@ -12700,8 +12732,10 @@ class HistoricalDataNodeExecutor(NodeExecutorBase):
12700
12732
  from programgarden_finance.ls.overseas_futureoption.chart.o3108.blocks import O3108InBlock
12701
12733
 
12702
12734
  # interval → gubun 변환 (0=일, 1=주, 2=월)
12703
- gubun_map = {"1d": "0", "1w": "1", "1M": "2"}
12704
- gubun = gubun_map.get(interval, "0")
12735
+ interval = normalize_futures_history_interval(interval)
12736
+ if interval not in FUTURES_DAILY_INTERVALS:
12737
+ raise ValueError("FUTURES_HISTORY_DAILY_INTERVAL_REQUIRED")
12738
+ gubun = FUTURES_DAILY_INTERVALS[interval]
12705
12739
 
12706
12740
  body = O3108InBlock(
12707
12741
  shcode=symbol,
@@ -12743,17 +12777,21 @@ class HistoricalDataNodeExecutor(NodeExecutorBase):
12743
12777
  context: ExecutionContext,
12744
12778
  node_id: str,
12745
12779
  ) -> List[Dict[str, Any]]:
12746
- """해외선물 분봉 조회 (o3103)"""
12780
+ """Read the initial o3103 page; dates and complete-range coverage are not supplied."""
12747
12781
  from programgarden_finance.ls.overseas_futureoption.chart.o3103.blocks import O3103InBlock
12748
12782
 
12749
12783
  # interval → ncnt 변환 (0=30초, 1=1분, 5=5분, 30=30분)
12750
- ncnt_map = {"30s": 0, "1min": 1, "5min": 5, "15min": 15, "30min": 30}
12751
- ncnt = ncnt_map.get(interval, 1)
12784
+ interval = normalize_futures_history_interval(interval)
12785
+ if interval not in FUTURES_MINUTE_INTERVALS:
12786
+ raise ValueError("FUTURES_HISTORY_MINUTE_INTERVAL_REQUIRED")
12787
+ ncnt = FUTURES_MINUTE_INTERVALS[interval]
12752
12788
 
12753
12789
  body = O3103InBlock(
12754
12790
  shcode=symbol,
12755
12791
  ncnt=ncnt,
12756
- readcnt=200, # 최대 조회 건수
12792
+ readcnt=200, # Existing application request size, not a documented LS maximum.
12793
+ cts_date="",
12794
+ cts_time="",
12757
12795
  )
12758
12796
 
12759
12797
  result = await api.chart().o3103(body=body).req_async()
@@ -0,0 +1,23 @@
1
+ """Explicit mappings for the existing single-page LS futures chart requests."""
2
+
3
+ MINUTE_INTERVALS = {"30s": 0, "1m": 1, "5m": 5, "15m": 15, "30m": 30}
4
+ DAILY_INTERVALS = {"1d": "0", "1w": "1", "1M": "2"}
5
+ INTERVAL_ALIASES = {"1min": "1m", "5min": "5m", "15min": "15m", "30min": "30m"}
6
+
7
+
8
+ def normalize_interval(interval):
9
+ """Preserve known legacy minute names without falling back to daily data."""
10
+ if isinstance(interval, str):
11
+ interval = INTERVAL_ALIASES.get(interval, interval)
12
+ if interval in MINUTE_INTERVALS or interval in DAILY_INTERVALS:
13
+ return interval
14
+ raise ValueError(
15
+ "UNSUPPORTED_FUTURES_HISTORY_INTERVAL: choose 30s, 1m, 5m, 15m, 30m, "
16
+ "1d, 1w or 1M. Hourly and unknown periods are not implemented; no daily fallback."
17
+ )
18
+
19
+
20
+ def validate_adjust(adjust):
21
+ """Never describe exact-contract prices as rollover-adjusted history."""
22
+ if adjust is not False:
23
+ raise ValueError("FUTURES_HISTORY_ADJUST_UNSUPPORTED: adjust must be false; rollover adjustment is not implemented")
@@ -345,6 +345,8 @@ class WorkflowResolver:
345
345
 
346
346
  # Order routing choices are literals, never prices or item bindings.
347
347
  self._validate_order_enums(workflow, registry, result)
348
+ self._validate_stock_history_interval(workflow, registry, result)
349
+ self._validate_futures_history_contract(workflow, registry, result)
348
350
 
349
351
  # 11. Static recommendations (topology analysis)
350
352
  for rec in run_static_recommendation_rules(
@@ -362,6 +364,55 @@ class WorkflowResolver:
362
364
 
363
365
  return result
364
366
 
367
+ def _validate_stock_history_interval(self, workflow, registry, result) -> None:
368
+ """Keep static/deep validation aligned with the stock chart executor."""
369
+ node_type = "OverseasStockHistoricalDataNode"
370
+ node_class = registry.get(node_type)
371
+ if node_class is None:
372
+ return
373
+ field = node_class.get_field_schema()["interval"]
374
+ for node in workflow.nodes:
375
+ if node.get("type") != node_type:
376
+ continue
377
+ if node.get("interval", field.default) in field.enum_values:
378
+ continue
379
+ result.add(build_error(
380
+ ErrorCode.INVALID_FIELD_ENUM,
381
+ "Stock historical intervals require a supported LS g3204 period; "
382
+ "intraday g3203 is not supported.",
383
+ location=ErrorLocation(
384
+ node_id=node.get("id"), node_type=node_type, field_path="interval",
385
+ ),
386
+ available_values=field.enum_values,
387
+ suggestion="Choose 1d, 1w, 1M or 1Y explicitly; do not substitute daily bars for an intraday strategy.",
388
+ ))
389
+
390
+ def _validate_futures_history_contract(self, workflow, registry, result) -> None:
391
+ """Keep native validation aligned with period and adjustment routing."""
392
+ from programgarden.futures_history import normalize_interval, validate_adjust
393
+
394
+ node_type = "OverseasFuturesHistoricalDataNode"
395
+ node_class = registry.get(node_type)
396
+ if node_class is None:
397
+ return
398
+ field = node_class.get_field_schema()["interval"]
399
+ for node in workflow.nodes:
400
+ if node.get("type") != node_type:
401
+ continue
402
+ for name, check, default, available in (
403
+ ("interval", normalize_interval, field.default, field.enum_values),
404
+ ("adjust", validate_adjust, False, ["false"]),
405
+ ):
406
+ try:
407
+ check(node.get(name, default))
408
+ except ValueError as exc:
409
+ result.add(build_error(
410
+ ErrorCode.INVALID_FIELD_ENUM, str(exc),
411
+ location=ErrorLocation(node_id=node.get("id"), node_type=node_type, field_path=name),
412
+ available_values=available,
413
+ suggestion="Choose a supported period and keep adjust=false; only exact-contract prices are implemented.",
414
+ ))
415
+
365
416
  def _validate_order_enums(self, workflow, registry, result) -> None:
366
417
  """Enforce fixed price/order types without changing dynamic close sides."""
367
418
  from programgarden_core.models.field_binding import ExpressionMode
@@ -5,7 +5,7 @@ authors = [
5
5
  homepage = "https://programgarden.com"
6
6
  requires-python = ">=3.12"
7
7
  name = "programgarden"
8
- version = "2.6.0"
8
+ version = "2.6.2"
9
9
  license = "AGPL-3.0-or-later"
10
10
  description = "ProgramGarden - 노드 기반 자동매매 DSL 실행 엔진"
11
11
  readme = "README.md"
@@ -36,7 +36,7 @@ litellm = ">=1.40.0"
36
36
  # 로 통째 실패한다(1.35.1 의 조용한 TypeError 보다 강한 모듈 로드 실패).
37
37
  # (1.26.0 이후로도 context.py 는 WorkflowPnLEvent(**event_data) 에 personal_metrics
38
38
  # 를 무조건 실으므로 그 하한 근거도 이 상한에 포함된다.)
39
- programgarden-core = "^2.6.0"
39
+ programgarden-core = "^2.6.2"
40
40
  programgarden-finance = "^2.0.1"
41
41
  programgarden-community = "^2.3.1"
42
42