diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..6f3c602
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,56 @@
+.PHONY: help ops:prepare ops:validate ops:build ops:data-collect ops:render ops:release ops:package full-gate
+
+help:
+ @echo "QuantEngine v0.1 — Operations CLI"
+ @echo ""
+ @echo "Core operations:"
+ @echo " make ops:render — Render operational report from packet"
+ @echo " make ops:validate — Validate release pipeline"
+ @echo " make ops:release — Full release DAG"
+ @echo " make ops:package — Package for deployment"
+ @echo " make full-gate — Strict validation (all gates must PASS)"
+ @echo ""
+ @echo "Data operations:"
+ @echo " make ops:prepare — Convert XLSX → JSON"
+ @echo " make ops:data-collect — KIS data collection"
+ @echo ""
+ @echo "Development:"
+ @echo " make dotnet:build — Build .NET projects"
+ @echo " make dotnet:run — Run Web API (port 8788)"
+ @echo " make dotnet:watch — Hot-reload API server"
+
+ops:prepare:
+ python tools/convert_xlsx_to_json.py
+
+ops:validate:
+ python tools/run_release_dag_v3.py --mode release
+
+ops:build:
+ python tools/build_bundle.py
+
+ops:data-collect:
+ python tools/run_kis_data_collection_v1.py --input-json GatherTradingData.json --sqlite-db src/quant_engine/kis_data_collection.db --output-json Temp/kis_data_collection_v1.json --kis-account real
+
+ops:render:
+ dotnet run --project src/dotnet/QuantEngine.Tools/QuantEngine.Tools.csproj -- report --packet=Temp/final_decision_packet_active.json --out=Temp/operational_report.json
+
+ops:release:
+ python tools/run_release_dag_v3.py --mode full
+
+ops:package:
+ python tools/refresh_trading_calendar.py && python tools/prepare_upload_zip.py --validation-mode release
+
+full-gate:
+ python tools/run_release_dag_v3.py --mode release --strict
+
+dotnet:build:
+ cd src/dotnet && dotnet build
+
+dotnet:run:
+ cd src/dotnet && dotnet run --project src/DataFeed.Api/QuantEngine.Web/QuantEngine.Web.csproj
+
+dotnet:watch:
+ cd src/dotnet && dotnet watch run --project src/QuantEngine.Web/QuantEngine.Web.csproj
+
+dotnet:test:
+ cd src/dotnet && dotnet test
diff --git a/src/dotnet/QuantEngine.Core/Interfaces/ICollectionRepository.cs b/src/dotnet/QuantEngine.Core/Interfaces/ICollectionRepository.cs
new file mode 100644
index 0000000..bbfe926
--- /dev/null
+++ b/src/dotnet/QuantEngine.Core/Interfaces/ICollectionRepository.cs
@@ -0,0 +1,56 @@
+namespace QuantEngine.Core.Interfaces;
+
+///
+/// Data collection repository (Dapper + PostgreSQL).
+/// Higher-level abstraction over IDataCollectionStore for Web API consumers.
+///
+public interface ICollectionRepository
+{
+ ///
+ /// Save new collection run.
+ ///
+ Task SaveRunAsync(CollectionRunRecord run);
+
+ ///
+ /// Update run with completion status.
+ ///
+ Task UpdateRunStatusAsync(string runId, string status, string? finishedAt = null, int? totalSnapshots = null, int? totalErrors = null);
+
+ ///
+ /// Save collection snapshot.
+ ///
+ Task SaveSnapshotAsync(CollectionSnapshotRecord snapshot);
+
+ ///
+ /// Save collection error.
+ ///
+ Task SaveErrorAsync(CollectionErrorRecord error);
+
+ ///
+ /// Fetch recent collection runs for UI dashboard.
+ ///
+ /// Number of runs to return (default: 20)
+ Task> GetRecentRunsAsync(int limit = 20);
+
+ ///
+ /// Fetch snapshots for a specific run.
+ ///
+ Task> GetRunSnapshotsAsync(string runId);
+
+ ///
+ /// Fetch errors for a specific run.
+ ///
+ /// Run ID
+ /// Max errors to return (default: 50)
+ Task> GetRunErrorsAsync(string runId, int limit = 50);
+
+ ///
+ /// Get collection pipeline dashboard state for Web UI.
+ ///
+ Task GetDashboardStateAsync();
+
+ ///
+ /// Fetch latest snapshots for a ticker across all datasets.
+ ///
+ Task> GetLatestSnapshotsForTickerAsync(string ticker, int limit = 10);
+}
diff --git a/src/dotnet/QuantEngine.Core/Interfaces/IDataCollectionStore.cs b/src/dotnet/QuantEngine.Core/Interfaces/IDataCollectionStore.cs
new file mode 100644
index 0000000..e12db28
--- /dev/null
+++ b/src/dotnet/QuantEngine.Core/Interfaces/IDataCollectionStore.cs
@@ -0,0 +1,95 @@
+namespace QuantEngine.Core.Interfaces;
+
+///
+/// Data collection storage abstraction layer.
+/// Maps Python data_collection_store_v1.py contracts to .NET.
+/// Supports PostgreSQL backend for production.
+///
+public interface IDataCollectionStore
+{
+ ///
+ /// Initialize storage tables and schema (idempotent).
+ ///
+ Task InitializeAsync();
+
+ ///
+ /// Insert or update collection run record.
+ ///
+ Task UpsertRunAsync(CollectionRunRecord run);
+
+ ///
+ /// Insert collection snapshot record.
+ ///
+ Task UpsertSnapshotAsync(CollectionSnapshotRecord snapshot);
+
+ ///
+ /// Append error record from collection attempt.
+ ///
+ Task AppendErrorAsync(CollectionErrorRecord error);
+
+ ///
+ /// Fetch recent collection runs for dashboard.
+ ///
+ /// Max number of runs to return
+ Task> FetchRecentRunsAsync(int limit = 20);
+
+ ///
+ /// Fetch latest snapshots for a ticker/dataset combination.
+ ///
+ Task> FetchLatestSnapshotsAsync(string ticker, string? datasetName = null, int limit = 10);
+
+ ///
+ /// Get collection pipeline dashboard state.
+ ///
+ Task GetDashboardStateAsync();
+}
+
+///
+/// Collection run record (maps Python CollectionRun).
+///
+public record CollectionRunRecord(
+ string RunId,
+ string Status,
+ string StartedAt,
+ string? FinishedAt = null,
+ int? TotalSnapshots = null,
+ int? TotalErrors = null,
+ string UpdatedAt = ""
+);
+
+///
+/// Collection snapshot record (maps Python CollectionSnapshot).
+///
+public record CollectionSnapshotRecord(
+ string RunId,
+ string DatasetName,
+ string Ticker,
+ string SourceName,
+ string PayloadJson,
+ string CapturedAt,
+ string CreatedAt = ""
+);
+
+///
+/// Collection error record (maps Python CollectionSourceError).
+///
+public record CollectionErrorRecord(
+ string RunId,
+ string SourceName,
+ string ErrorKind,
+ string ErrorMessage,
+ string Ticker = "",
+ string CreatedAt = ""
+);
+
+///
+/// Dashboard state summary.
+///
+public record CollectionDashboardStateRecord(
+ string? LastRunId,
+ string? LastRunStatus,
+ string? LastFinishedAt,
+ int TotalSnapshots,
+ int TotalErrors,
+ List RecentErrors
+);
diff --git a/src/dotnet/QuantEngine.Core/Interfaces/ITokenCache.cs b/src/dotnet/QuantEngine.Core/Interfaces/ITokenCache.cs
new file mode 100644
index 0000000..646e7b2
--- /dev/null
+++ b/src/dotnet/QuantEngine.Core/Interfaces/ITokenCache.cs
@@ -0,0 +1,28 @@
+namespace QuantEngine.Core.Interfaces;
+
+///
+/// Token caching for KIS API authentication.
+/// Replaces Python's sqlite3-based token storage with PostgreSQL.
+///
+public interface ITokenCache
+{
+ ///
+ /// Retrieve cached access token if valid and not near expiration.
+ ///
+ /// Account type: "real" or "mock"
+ /// Access token if cached and valid; null if expired or missing
+ Task GetCachedTokenAsync(string account);
+
+ ///
+ /// Store access token with expiration time.
+ ///
+ /// Account type: "real" or "mock"
+ /// Access token
+ /// Token expiration time (UTC)
+ Task SaveTokenAsync(string account, string token, DateTime expiresAt);
+
+ ///
+ /// Clear expired tokens and stale entries.
+ ///
+ Task ClearExpiredTokensAsync();
+}