@avytheone/efmesh 0.1.0-beta.2 → 0.2.1

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.
package/CHANGELOG.md CHANGED
@@ -1,102 +1,181 @@
1
1
  # Changelog
2
2
 
3
- Формат — [Keep a Changelog](https://keepachangelog.com/ru/1.1.0/),
4
- версионирование — [SemVer](https://semver.org/lang/ru/).
5
- Внутренняя история разработки велась фазами F0–F6 (SPEC.md §13);
6
- первая версия собирает их целиком.
7
-
8
- ## [Unreleased]
3
+ Format — [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
4
+ versioning — [SemVer](https://semver.org/).
5
+ Internal development history was tracked in phases F0–F6 (SPEC.md §13);
6
+ the first version gathers them in full.
7
+
8
+ ## [0.2.1] — 2026-07-16
9
+
10
+ Theme: hygiene for the first stranger — the whole surface is English,
11
+ failures explain themselves, execution is visible.
12
+
13
+ - Detailed execution log (#14). `apply` and `run` now narrate their work
14
+ through Effect's logging system: per-model build start/finish with duration,
15
+ backfill batch progress (`batch n of m` with interval bounds), warn-audits
16
+ and promotion. Levels — **info** for lifecycle (visible by default), **warn**
17
+ for warn-audits/retries, **debug** for the rendered SQL and lock internals;
18
+ the existing `--log-level` flag sets the minimum. Every line carries
19
+ structured fields (`model`, `env`, `interval`) as annotations. Logs go to
20
+ **stderr** (stdout and `--json` stay byte-clean); a TTY gets pretty colored
21
+ output, a pipe/journal gets one-line logfmt with no ANSI. Embedders provide
22
+ their own `Logger` layer to redirect sinks and levels. Exit codes unchanged.
23
+ - Human-readable, precise errors everywhere (#13). Every tagged error now
24
+ derives its `message` from its typed fields, so the culprit (model, env,
25
+ file, interval) and the underlying engine/system text are always present —
26
+ an empty `EngineError:` is constructively impossible. `EngineError` carries
27
+ the failing model and the engine's own message; a failing `apply` names the
28
+ model, quotes DuckDB/Postgres verbatim, and shows the SQL context. The CLI
29
+ now renders one failure screen (cause first, an actionable hint where one
30
+ exists) and prints the Effect fiber trace only under `--log-level debug`.
31
+ Exit codes (0/1/2) and `--json` shapes are unchanged. New exported error
32
+ `UnknownModelError` (render/lineage against a name not in the project).
33
+ - The entire user-facing surface is English: CLI output and help, error
34
+ messages, `--json` string values (key names and exit codes unchanged),
35
+ the `init` scaffold, the hospital example data (#11). Source comments
36
+ and test names too (#12).
37
+ - **Breaking:** the `apply` confirmation prompt accepts only `y`/`yes`
38
+ (case-insensitive); the Cyrillic tokens are no longer recognized.
39
+ - `efmesh init` scaffold now teaches the core lifecycle: a seed feeding
40
+ an incremental-by-time-range model with a blocking audit and a full
41
+ rollup on top, runnable immediately after `init` (#11).
42
+
43
+ ## [0.2.0] — 2026-07-16
44
+
45
+ Theme: "operator and team" — efmesh in the hands of a non-author: the
46
+ operator of the nightly cron and a team with CI.
47
+
48
+ - `efmesh status <env>` — what is going on in one command: the last applied
49
+ plan, interval lag per incremental model, failed intervals, recent run
50
+ ticks (#1).
51
+ - Run tick journal in the state store (`runs`, schema v4 — `efmesh migrate`
52
+ with a store backup): every tick records its outcome, including
53
+ unsuccessful ones (#2).
54
+ - `--json` on `plan`, `audit` and `status`: a stable machine-readable shape
55
+ for CI and bots; exit codes unchanged (#3).
56
+ - Canonicalization cache in the state store (schema v5): a no-op plan on
57
+ 2000 models drops from ~0.6 s to ~50 ms; the cache key includes the
58
+ dialect and `FINGERPRINT_VERSION`, so canon drift can never be masked (#8).
59
+ - Integration test: stale lock reclaim under a real `kill -9` (#7).
60
+ - `plan --explain`: for every change — which canonical-AST nodes diverged
61
+ and why the category followed (cascade sources for `indirect`, inherited
62
+ physics for `forward-only`); also shipped in `--json` as `explain`
63
+ (`PlanAction.explain` in the library API). AST paths are a debugging
64
+ hint, not a contract (#4).
65
+ - Indirect physics reuse — the sqlmesh "indirect non-breaking" class: a
66
+ descendant whose own AST did not change and whose changed parents are all
67
+ non-breaking/forward-only inherits the previous version's physical table
68
+ and interval accounting instead of a full rebuild (scdType2 keeps its row
69
+ history). Safety is proven, not assumed: the fingerprint recomputed with
70
+ the parents' old fingerprints must reproduce the old version, so
71
+ simultaneous metadata drift disables reuse (#5).
72
+ - `--reclassify model=breaking|non-breaking` on `plan`/`apply`: the
73
+ operator's verdict as a flag on top of `--explain` (no interactive
74
+ dialog — works in CI), journaled with `applied_by`
75
+ (`PlanAction.reclassifiedFrom`); governs descendants' physics reuse.
76
+ Guard rail: dropped columns declared non-breaking are refused (#5).
77
+ - `diff --data` — compare the DATA of two environments: full row counts,
78
+ key overlap (grain or the kind's key), per-column mismatch rates among
79
+ matched keys, schema drift between sides. `--sample P` compares a
80
+ deterministic share of keys (md5 buckets aligned across both sides — no
81
+ false only-in rows); `--json` for CI (`dataDiffEnvironments` in the
82
+ library API) (#6).
83
+ - `efmesh schedule <env>` — register the `run` tick in the OS scheduler via
84
+ `Bun.cron` (crontab/launchd/Task Scheduler; `engines.bun >= 1.3.11`).
85
+ Idempotent by title; `--remove`, `--list`; on cron-less Linux
86
+ (Arch family) it detects the missing daemon and `--print-systemd` emits
87
+ user units with `Persistent=true` instead (#10).
9
88
 
10
89
  ## [0.1.0-beta.2] — 2026-07-16
11
90
 
12
- - Релизный конвейер: публикация в npmjs.org из GitHub Actions по тегу
13
- `v*` через Trusted Publishing (OIDC, без токенов) с provenance.
14
- Содержимое пакета не менялосьрелиз проверяет сам конвейер.
91
+ - Release pipeline: publishing to npmjs.org from GitHub Actions on a `v*`
92
+ tag via Trusted Publishing (OIDC, no tokens) with provenance.
93
+ The package contents did not change the release verifies the pipeline itself.
15
94
 
16
95
  ## [0.1.0-beta.1] — 2026-07-16
17
96
 
18
- Первая публичная бета. Всё ниже новое.
19
-
20
- ### Модели и план (F0–F1)
21
-
22
- - `defineModel`/`defineExternal`: модели TypeScript-модули, ссылки
23
- импорты, форма данных Effect Schema; DAG строится из значений.
24
- - Fingerprint по каноническому AST (`json_serialize_sql` DuckDB):
25
- переформатирование SQL не триггерит пересборку.
26
- - Снапшоты версий, виртуальные окружения (view поверх физики),
27
- план как diff, промоушен без пересчёта.
28
- - `kind.incrementalByTimeRange`: интервальный учёт в state store, бэкфилл
29
- батчами DELETE+INSERT в транзакции, resume с места остановки, `lookback`.
30
- - Контракт схемы перед сборкой (`DESCRIBE` против объявленной Schema).
31
- - `target: "parquet"`: озеро локально или на S3 (httpfs), интервал = партиция.
32
-
33
- ### Качество и эксплуатация (F2)
34
-
35
- - Аудиты `notNull`/`unique`/`accepted`, blocking/warn.
36
- - `testModel` (`efmesh/testing`): юнит-тесты моделей на фикстурах
37
- в in-memory DuckDB.
38
- - Категоризация изменений breaking / non-breaking / indirect по AST.
39
- - `efmesh run`: идемпотентный тик планировщика с межпроцессным локом;
40
- `Runner.daemon` для встраивания.
97
+ First public beta. Everything below is new.
98
+
99
+ ### Models and plans (F0–F1)
100
+
101
+ - `defineModel`/`defineExternal`: models are TypeScript modules, references
102
+ are imports, data shape is Effect Schema; the DAG is built from values.
103
+ - Fingerprint over the canonical AST (`json_serialize_sql` in DuckDB):
104
+ reformatting SQL does not trigger a rebuild.
105
+ - Version snapshots, virtual environments (views over physical storage),
106
+ a plan as a diff, promotion without recomputation.
107
+ - `kind.incrementalByTimeRange`: interval ledger in the state store, backfill
108
+ in DELETE+INSERT batches within a transaction, resume from where it stopped, `lookback`.
109
+ - A schema contract before a build (`DESCRIBE` against the declared Schema).
110
+ - `target: "parquet"`: a lake locally or on S3 (httpfs), interval = partition.
111
+
112
+ ### Quality and operations (F2)
113
+
114
+ - `notNull`/`unique`/`accepted` audits, blocking/warn.
115
+ - `testModel` (`efmesh/testing`): unit tests for models on fixtures
116
+ in in-memory DuckDB.
117
+ - Change categorization breaking / non-breaking / indirect by AST.
118
+ - `efmesh run`: an idempotent scheduler tick with a cross-process lock;
119
+ `Runner.daemon` for embedding.
41
120
  - `efmesh janitor`, `efmesh diff`, `defineSeed`,
42
- `kind.incrementalByUniqueKey` (upsert), экспорт витрин в ATTACH-базы,
43
- метрики и спаны.
44
-
45
- ### Широта (F3)
46
-
47
- - Postgres: движок на `Bun.SQL` (пул, параллельные батчи бэкфилла),
48
- canonicalize через libpg_query, state store в схеме `efmesh_state`.
49
- - `--forward-only`: правка без переигрывания историиновая версия
50
- наследует физику и done-интервалы, новые колонки через `ALTER`.
51
- - `kind.scdType2` (история строк), `kind.embedded` (подзапрос без
52
- материализации), `defineSqlModel` (сырые `.sql` с `@ref`/`@start`/`@end`).
53
- - Колоночный `efmesh lineage`, `efmesh graph --html`.
54
-
55
- ### Эксплуатационная зрелость (F4)
56
-
57
- - Межмодельная DAG-конкурентность apply (`--jobs`): модель стартует
58
- по готовности родителей; на DuckDB честно 1 (одно соединение).
59
- - `target: "ducklake"`: физика в DuckLake-каталоге, снапшоты и time travel
60
- каталога бонус.
61
- - Автономный `efmesh audit <env>` по view-слою окружения.
62
- - `efmesh init` (скаффолд), версия схемы state store + `efmesh migrate`.
63
- - Подтверждение плана в TTY (`--yes` пропускает, не-TTY едет без вопроса).
64
-
65
- ### Бета-гейт (F5)
66
-
67
- - Межпроцессный лок на `apply` — общий env-лок с `run`: параллельные
68
- мутации окружения из разных процессов взаимно исключаются
69
- (`LockHeldError`); у janitor свой глобальный лок.
70
- - Discovery моделей по glob: `discovery: "models/**/*.ts"` в конфиге.
71
- - Ретраи батча бэкфилла: `--retries N`, `Schedule.exponential`;
72
- аудиты не ретраятся.
73
- - `applied_by` в журнале планов (версия схемы стора 2).
74
- - Лицензия MIT.
75
-
76
- ### Бета-гейт, часть 2 (F6)
77
-
78
- - effect peerDependency с точным пином беты; еженедельный CI против
79
- свежей беты (ранний сигнал дрейфа v4).
80
- - Fingerprint как контракт: `FINGERPRINT_VERSION` в снапшотах (схема
81
- стора 3), golden-тесты замораживают канонизацию DuckDB и libpg_query;
82
- снапшот чужой версии громкая `FingerprintVersionError`.
83
- - Гонка janitor↔apply закрыта транзакционно: claim сироты атомарен
84
- с проверками, воскрешение снимает сиротство, промоушен проверяет
85
- живость снапшотов — view не переключается на снесённую физику.
86
- - Parquet-партиции пишутся атомарно (temp + rename); `migrate` снимает
87
- копию SQLite-стора перед апгрейдом схемы.
88
- - **Ломающее:** `apply` в не-TTY с изменениями требует `--yes`;
89
- exit-код `2` = «ждёт человека» (отказ подтверждения,
121
+ `kind.incrementalByUniqueKey` (upsert), export of marts to ATTACH databases,
122
+ metrics and spans.
123
+
124
+ ### Breadth (F3)
125
+
126
+ - Postgres: an engine on `Bun.SQL` (pool, parallel backfill batches),
127
+ canonicalize via libpg_query, state store in the `efmesh_state` schema.
128
+ - `--forward-only`: a change without replaying history the new version
129
+ inherits physical storage and done-intervals, new columns via `ALTER`.
130
+ - `kind.scdType2` (row history), `kind.embedded` (subquery without
131
+ materialization), `defineSqlModel` (raw `.sql` with `@ref`/`@start`/`@end`).
132
+ - Column-level `efmesh lineage`, `efmesh graph --html`.
133
+
134
+ ### Operational maturity (F4)
135
+
136
+ - Cross-model DAG concurrency for apply (`--jobs`): a model starts
137
+ as soon as its parents are ready; on DuckDB it is honestly 1 (a single connection).
138
+ - `target: "ducklake"`: physical storage in a DuckLake catalog, catalog
139
+ snapshots and time travel come as a bonus.
140
+ - Standalone `efmesh audit <env>` over an environment's view layer.
141
+ - `efmesh init` (scaffold), state-store schema version + `efmesh migrate`.
142
+ - Plan confirmation in a TTY (`--yes` skips it, a non-TTY proceeds without asking).
143
+
144
+ ### Beta gate (F5)
145
+
146
+ - Cross-process lock on `apply` — a shared env lock with `run`: parallel
147
+ mutations of an environment from different processes are mutually exclusive
148
+ (`LockHeldError`); the janitor has its own global lock.
149
+ - Model discovery by glob: `discovery: "models/**/*.ts"` in the config.
150
+ - Backfill batch retries: `--retries N`, `Schedule.exponential`;
151
+ audits are not retried.
152
+ - `applied_by` in the plans journal (store schema version 2).
153
+ - MIT license.
154
+
155
+ ### Beta gate, part 2 (F6)
156
+
157
+ - effect is a peerDependency with an exact beta pin; a weekly CI job against
158
+ a fresh beta (an early signal of v4 drift).
159
+ - Fingerprint as a contract: `FINGERPRINT_VERSION` in snapshots (store
160
+ schema 3), golden tests freeze DuckDB and libpg_query canonicalization;
161
+ a snapshot of a foreign version is a loud `FingerprintVersionError`.
162
+ - The janitor↔apply race is closed transactionally: claiming an orphan is
163
+ atomic with the checks, resurrection clears the orphan status, promotion
164
+ checks snapshot liveness a view never switches to removed physical storage.
165
+ - Parquet partitions are written atomically (temp + rename); `migrate` takes
166
+ a copy of the SQLite store before upgrading the schema.
167
+ - **Breaking:** `apply` in a non-TTY with changes requires `--yes`;
168
+ exit code `2` = "awaiting a human" (confirmation refused,
90
169
  `RunBlockedByChangesError`).
91
- - Публичное API сужено до осознанного whitelist (`index.ts`).
92
- - README на английском (русское зеркало README.ru.md), секция
93
- «для кого это (и для кого нет)».
94
-
95
- ### Известные ограничения
96
-
97
- - Effect v4 beta-зависимость; API efmesh держится стабильного
98
- подмножества.
99
- - Одиночный бинарник `bun build --compile` собирается, но standalone-Bun
100
- не резолвит импорт `"efmesh"` из рантайм-конфигадистрибуция пакетом.
101
- - Nullability не входит в контракт схемы (DuckDB `DESCRIBE` её не отдаёт) —
102
- выражается аудитом `notNull`.
170
+ - The public API is narrowed to a deliberate whitelist (`index.ts`).
171
+ - README in English (the Russian mirror is README.ru.md), a
172
+ "who this is for (and who it isn't)" section.
173
+
174
+ ### Known limitations
175
+
176
+ - Effect v4 is a beta dependency; the efmesh API sticks to a stable
177
+ subset.
178
+ - A single `bun build --compile` binary builds, but standalone Bun
179
+ cannot resolve the `"efmesh"` import from a runtime config distribution is via the package.
180
+ - Nullability is not part of the schema contract (DuckDB `DESCRIBE` does not report it) —
181
+ it is expressed with the `notNull` audit.
package/README.md CHANGED
@@ -2,12 +2,14 @@
2
2
 
3
3
  > Data transformation in the spirit of [sqlmesh](https://sqlmesh.com) — on TypeScript, [Bun](https://bun.sh) and [Effect](https://effect.website).
4
4
 
5
- [![ci](https://github.com/avytheone/efmesh/actions/workflows/ci.yml/badge.svg)](https://github.com/avytheone/efmesh/actions/workflows/ci.yml) ![status](https://img.shields.io/badge/status-beta-orange) ![version](https://img.shields.io/badge/version-0.1.0--beta.1-blue) ![license](https://img.shields.io/badge/license-MIT-green) ![runtime](https://img.shields.io/badge/runtime-bun-black) ![effect](https://img.shields.io/badge/effect-v4-5C4EE5)
5
+ [![ci](https://github.com/avytheone/efmesh/actions/workflows/ci.yml/badge.svg)](https://github.com/avytheone/efmesh/actions/workflows/ci.yml) ![status](https://img.shields.io/badge/status-beta-orange) ![npm](https://img.shields.io/npm/v/%40avytheone%2Fefmesh) ![license](https://img.shields.io/badge/license-MIT-green) ![runtime](https://img.shields.io/badge/runtime-bun-black) ![effect](https://img.shields.io/badge/effect-v4-5C4EE5)
6
6
 
7
7
  *Русская версия: [README.ru.md](./README.ru.md).*
8
8
 
9
9
  Models are plain TypeScript modules: SQL bodies, imports as dependencies, Effect Schema as the data shape. efmesh fingerprints every model by its canonical AST, keeps versions as snapshots, computes a plan as the diff between your project and an environment, and applies exactly that plan: physical tables are rebuilt only where something actually changed, while environments (dev/prod/…) are virtual views over shared physical storage — promoting to prod costs zero recomputation.
10
10
 
11
+ <p align="center"><img src="docs/demo.svg" alt="efmesh demo: a ref typo is a compile error; a plan rebuilds exactly the changed branch; promotion is a view swap" width="840"></p>
12
+
11
13
  ```ts
12
14
  import { Schema } from "effect"
13
15
  import { defineModel, kind } from "@avytheone/efmesh"
@@ -46,9 +48,15 @@ export const moves = defineModel(
46
48
 
47
49
  A typo in a `ref` is a compile error, not an empty run; renaming a parent's column breaks the child's build before any SQL reaches the database.
48
50
 
51
+ ## Why this exists: small data lakes
52
+
53
+ Most analytics in the world is not a cloud warehouse — it is DuckDB-class data: gigabytes to a terabyte on one machine. Product analytics of a startup, the marts of one department, on-prem and edge deployments, a pipeline living inside a SaaS app. dbt and sqlmesh were born in the cloud-DWH world and carry that weight with them (Python, adapters, infrastructure). efmesh is the sqlmesh approach that is `bun add` and go — and the lake is a folder of parquet files.
54
+
55
+ Honestly placed: the **core** is the same class of system as sqlmesh — snapshots, AST fingerprints, plans as diffs, virtual environments. The **breadth** is not: no cloud engines, no multi-dialect, no web UI, no ecosystem of packages — and no ambition to catch up on all of it. Compared to dbt-core the trade is reversed: dbt has an industry around it, but no state-based plans, no virtual environments, no fingerprint versioning — every team reinvents "how not to rebuild everything". Our four honest advantages: **types as the DAG contract** (a typo breaks the build, not the nightly run), **library before CLI** (embed `Efmesh.apply(...)` in your app), **one language for the whole stack** (models, app, tests), and a **codebase you can read in an evening** (~5.5k lines).
56
+
49
57
  ## Who this is for (and who it isn't)
50
58
 
51
- **For you**, if you are a TypeScript team on Bun, want a typed dbt/sqlmesh-style workflow on top of DuckDB or Postgres, and are fine living on a beta (efmesh is 0.1.x; Effect v4 is beta, pinned exactly as a peer dependency).
59
+ **For you**, if you are a TypeScript team on Bun, want a typed dbt/sqlmesh-style workflow on top of DuckDB or Postgres, and are fine living on a beta (efmesh is 0.2.x; Effect v4 is beta, pinned exactly as a peer dependency).
52
60
 
53
61
  **Not for you**, if you need: a Node runtime (Bun-only for now), multi-dialect or cloud DWHs (Snowflake/BigQuery are out of scope), 1.0-grade stability, or the Python ecosystem — take sqlmesh instead, honestly.
54
62
 
@@ -58,7 +66,7 @@ A typo in a `ref` is a compile error, not an empty run; renaming a parent's colu
58
66
 
59
67
  **Materialization targets.** Native engine tables, `parquet` (a lake, local or s3://, interval = partition, views over `read_parquet`), `ducklake` (table-per-fingerprint in a [DuckLake](https://ducklake.select) catalog — catalog snapshots and time travel come as a bonus).
60
68
 
61
- **Plans and versions.** Fingerprints over canonical ASTs (reformatting SQL never triggers a rebuild — frozen by golden tests), change categorization breaking / non-breaking / indirect / forward-only, `--forward-only` applies a change without replaying history (the new version inherits physical storage and done-intervals; new columns via `ALTER`), plan confirmation in a TTY, an applied-plans journal with `applied_by`.
69
+ **Plans and versions.** Fingerprints over canonical ASTs (reformatting SQL never triggers a rebuild — frozen by golden tests), change categorization breaking / non-breaking / indirect / forward-only with `plan --explain` reasoning and a `--reclassify` operator override, indirect physics reuse (descendants of a non-breaking change are not rebuilt — scdType2 keeps its history), `--forward-only` applies a change without replaying history (the new version inherits physical storage and done-intervals; new columns via `ALTER`), plan confirmation in a TTY, an applied-plans journal with `applied_by`.
62
70
 
63
71
  **Data quality.** A schema contract before every build (`DESCRIBE` of the query against the declared Schema), `notNull` / `unique` / `accepted` audits (blocking fails the apply, `warn` logs), a standalone `efmesh audit` over an environment's view layer, and `testModel` — unit tests for models on fixtures in in-memory DuckDB.
64
72
 
@@ -69,7 +77,7 @@ A typo in a `ref` is a compile error, not an empty run; renaming a parent's colu
69
77
  ## Quickstart
70
78
 
71
79
  ```sh
72
- bun add -d @avytheone/efmesh@beta
80
+ bun add -d @avytheone/efmesh
73
81
  bunx efmesh init my-warehouse && cd my-warehouse
74
82
  bunx efmesh plan dev # what would be done
75
83
  bunx efmesh apply dev # physical tables, backfill, view layer
@@ -102,7 +110,7 @@ models (TS modules) ──► DAG + fingerprints over canonical ASTs
102
110
  - **Virtual layer** — views `<env>__<schema>.<table>` (prod is just `<schema>.<table>`) pointing at physical storage. An environment is a set of pointers; promotion and rollback are view swaps.
103
111
  - **The interval ledger** is the single source of truth about what has been computed: an interrupted backfill resumes where it stopped; recomputing an interval is a transactional DELETE+INSERT of the range — no duplicates.
104
112
 
105
- Full architecture, invariants and decisions: [SPEC.md](./SPEC.md) (in Russian).
113
+ Full architecture, invariants and decisions: [SPEC.md](./SPEC.md).
106
114
 
107
115
  ## Data quality
108
116
 
@@ -157,18 +165,84 @@ export default defineConfig({
157
165
  | `efmesh plan <env>` | diff the project against an environment + missing intervals; changes nothing |
158
166
  | `efmesh apply <env>` | plan → confirmation (TTY) → physical tables, backfill, view layer |
159
167
  | `efmesh run <env>` | scheduler tick: new intervals only, under the lock; for cron |
168
+ | `efmesh status <env>` | what is going on: last plan, interval lag, recent run ticks |
160
169
  | `efmesh audit <env>` | audit the environment's view layer — catches after-the-fact degradation |
161
- | `efmesh diff <envA> <envB>` | how two environments differ |
170
+ | `efmesh diff <envA> <envB>` | how two environments differ; `--data` compares the actual data |
162
171
  | `efmesh render <model> [--env]` | the final SQL of a model |
163
172
  | `efmesh lineage <model[.col]>` | column lineage down to the raw sources |
164
173
  | `efmesh graph [--html]` | the model DAG as text or a page |
165
174
  | `efmesh janitor [--ttl 7]` | remove orphaned physical storage older than ttl |
166
175
  | `efmesh migrate` | bring the state-store schema up to the current version |
176
+ | `efmesh schedule <env>` | register `run <env>` in the OS scheduler via `Bun.cron` |
167
177
 
168
178
  `apply`/`run` flags: `--jobs N` — DAG concurrency (always 1 on DuckDB — single connection), `--retries N` — retries for transient batch failures (exponential backoff), `--yes`/`-y` — skip confirmation, `--forward-only <model>,…` — reuse physical storage and history.
169
179
 
180
+ `plan`/`apply` take `--reclassify <model>=breaking|non-breaking[,…]` — the
181
+ operator's verdict on top of `--explain`, journaled with `applied_by`. A
182
+ non-breaking parent lets unchanged descendants reuse their previous physical
183
+ tables instead of rebuilding (scdType2 keeps its row history); an override
184
+ that plainly contradicts the AST (dropped columns) is refused.
185
+
186
+ `plan`, `audit` and `status` take `--json` — a stable machine-readable shape
187
+ (a contract under semver) for CI and bots; exit codes are unchanged, stdout
188
+ stays pure JSON.
189
+
190
+ `plan --explain` adds the reasoning to every change: which canonical-AST
191
+ nodes diverged (`where_clause`, `select_list[2] (added)`, …) and why the
192
+ category followed — including cascade sources for `indirect`. The same
193
+ data ships in `--json` as `explain`; the AST paths are a debugging hint,
194
+ not part of the contract.
195
+
196
+ `diff <envA> <envB> --data` compares the actual data of two environments:
197
+ row counts, key overlap (grain or the kind's key), per-column mismatch
198
+ rates among matched keys, schema drift between sides. `--sample P` (1–99)
199
+ compares a deterministic share of keys — md5 buckets aligned across both
200
+ sides, so sampling never fabricates only-in rows. `--model a,b` narrows,
201
+ `--json` for CI.
202
+
203
+ `schedule <env> [--cron '@hourly']` registers the `run` tick in the OS
204
+ scheduler (crontab / launchd / Task Scheduler) via `Bun.cron` — idempotent
205
+ by title, `--remove` unregisters, `--list` shows what's there. Honest
206
+ caveats: OS cron runs in the local timezone and does not catch up on missed
207
+ runs, and Arch-family Linux ships no cron daemon at all — `--print-systemd`
208
+ emits user-unit files instead (`Persistent=true` catches up). Overlapping
209
+ ticks are safe by construction: `run` takes the env lock and exits `2` when
210
+ changes await a human.
211
+
170
212
  Exit codes: `0` — success, `1` — error, `2` — "awaiting a human": the plan needs confirmation in a non-TTY (add `--yes`), or `run` hit unapplied changes. In a non-TTY, `apply` with changes and no `--yes` refuses — efmesh will not silently roll out a plan nobody has seen.
171
213
 
214
+ ## Logging
215
+
216
+ `apply` and `run` narrate what they do. Logs go to **stderr** — stdout stays
217
+ reserved for the plan screen, summaries and `--json`, which stays byte-clean.
218
+ Levels, set by the built-in `--log-level` flag (minimum level, default `info`):
219
+
220
+ - **info** — lifecycle a human watches: per-model build start/finish with
221
+ duration, backfill batch progress (`batch 3 of 7` with the interval bounds),
222
+ promotion.
223
+ - **warn** — warn-audits (violations that do not block) and retries.
224
+ - **debug** — the rendered SQL about to run, lock acquire/release, and other
225
+ internals. `--log-level debug` also prints the full fiber trace on a failure.
226
+
227
+ Each line carries structured fields as annotations (`model`, `env`, `interval`,
228
+ …). At a TTY the output is pretty and colored; piped to a file or the systemd
229
+ journal it is one-line [logfmt](https://brandur.org/logfmt) with no ANSI, so a
230
+ log reader (or an AI agent post-morteming a 3am tick) can group by field.
231
+
232
+ Embedding efmesh as a library? Logging is Effect's `Effect.log*` — provide your
233
+ own `Logger` layer (sink, format, minimum level) and the CLI's choices do not
234
+ apply. Row counts are not logged: efmesh never runs an extra query just to count.
235
+
236
+ ## Performance
237
+
238
+ The framework overhead is negligible for any realistic project (in-memory DuckDB, `bun bench/plan-bench.ts N`):
239
+
240
+ | models | cold plan | apply (all physical) | no-op plan | promote to prod |
241
+ |---|---|---|---|---|
242
+ | 100 | 54 ms | 158 ms | 3 ms | 51 ms |
243
+ | 500 | 228 ms | 759 ms | 11 ms | 197 ms |
244
+ | 2000 | 0.9 s | 2.9 s | 50 ms | 1.3 s |
245
+
172
246
  ## Postgres
173
247
 
174
248
  ```ts
@@ -186,10 +260,10 @@ Next up: categorization override in the plan dialog, non-time-based intervals. K
186
260
 
187
261
  ## Documentation
188
262
 
189
- - [SPEC.md](./SPEC.md) — the architecture spec: decisions, invariants, open questions (in Russian);
190
- - [CHANGELOG.md](./CHANGELOG.md) — release history (in Russian);
263
+ - [SPEC.md](./SPEC.md) — the architecture spec: decisions, invariants, open questions;
264
+ - [CHANGELOG.md](./CHANGELOG.md) — release history;
191
265
  - [examples/hospital](./examples/hospital/) — a live example with every model kind;
192
- - [CONTRIBUTING.md](./CONTRIBUTING.md) — build, test and PR guide (in Russian; PRs and issues in English are welcome).
266
+ - [CONTRIBUTING.md](./CONTRIBUTING.md) — build, test and PR guide.
193
267
 
194
268
  ## License
195
269
 
package/README.ru.md CHANGED
@@ -4,10 +4,12 @@
4
4
 
5
5
  *Русское зеркало; основной README — [английский](./README.md).*
6
6
 
7
- [![ci](https://github.com/avytheone/efmesh/actions/workflows/ci.yml/badge.svg)](https://github.com/avytheone/efmesh/actions/workflows/ci.yml) ![status](https://img.shields.io/badge/status-beta-orange) ![version](https://img.shields.io/badge/version-0.1.0--beta.1-blue) ![license](https://img.shields.io/badge/license-MIT-green) ![runtime](https://img.shields.io/badge/runtime-bun-black) ![effect](https://img.shields.io/badge/effect-v4-5C4EE5)
7
+ [![ci](https://github.com/avytheone/efmesh/actions/workflows/ci.yml/badge.svg)](https://github.com/avytheone/efmesh/actions/workflows/ci.yml) ![status](https://img.shields.io/badge/status-beta-orange) ![npm](https://img.shields.io/npm/v/%40avytheone%2Fefmesh) ![license](https://img.shields.io/badge/license-MIT-green) ![runtime](https://img.shields.io/badge/runtime-bun-black) ![effect](https://img.shields.io/badge/effect-v4-5C4EE5)
8
8
 
9
9
  Модели — обычные TypeScript-модули: тело на SQL, ссылки между моделями — импорты, форма данных — Effect Schema. efmesh считает fingerprint каждой модели по каноническому AST, хранит версии снапшотами, строит план как diff «проект против окружения» и применяет ровно его: физика пересчитывается только там, где что-то реально изменилось, а окружения (dev/prod/…) — виртуальные view поверх общей физики, промоушен в prod не стоит ни одного пересчёта.
10
10
 
11
+ <p align="center"><img src="docs/demo.svg" alt="efmesh demo: a ref typo is a compile error; a plan rebuilds exactly the changed branch; promotion is a view swap" width="840"></p>
12
+
11
13
  ```ts
12
14
  import { Schema } from "effect"
13
15
  import { defineModel, kind } from "@avytheone/efmesh"
@@ -46,11 +48,17 @@ export const moves = defineModel(
46
48
 
47
49
  Опечатка в `ref` у нас — ошибка компиляции, а не пустой прогон; переименованная колонка родителя ломает сборку потомка до того, как SQL уедет в базу.
48
50
 
51
+ ## Зачем это существует: маленькие озёра данных
52
+
53
+ Большинство аналитики в мире — не облачный warehouse, а данные DuckDB-класса: от гигабайт до терабайта на одной машине. Продуктовая аналитика стартапа, витрины одного отдела, on-prem и edge, pipeline внутри SaaS-приложения. dbt и sqlmesh родом из мира облачных DWH и тащат этот вес с собой (Python, адаптеры, инфраструктура). efmesh — это sqlmesh-подход, который `bun add` и поехал, а озеро — это папка parquet-файлов.
54
+
55
+ Честная позиция: **ядро** — тот же класс систем, что sqlmesh (снапшоты, AST-fingerprint, план как diff, виртуальные окружения), но **широты** нет: ни облачных движков, ни мульти-диалекта, ни веб-UI, ни экосистемы пакетов — и нет амбиции всё это догонять. С dbt-core размен обратный: у dbt индустрия вокруг, но нет state-based планов, виртуальных окружений и версионирования по fingerprint — каждая команда сама изобретает «как не пересчитывать всё». Наши четыре честных преимущества: **типы как контракт DAG** (опечатка ломает сборку, а не ночной прогон), **библиотека прежде CLI** (`Efmesh.apply(...)` встраивается в приложение), **один язык на весь стек** (модели, приложение, тесты) и **кодовая база, читаемая за вечер** (~5.5k строк).
56
+
49
57
  ## Для кого это (и для кого нет)
50
58
 
51
59
  **Для вас**, если вы TypeScript-команда на Bun, хотите типизированный
52
60
  dbt/sqlmesh-подход поверх DuckDB или Postgres и готовы жить на бете
53
- (efmesh — 0.1.x, Effect v4 — beta, версия effect пинована peer-зависимостью).
61
+ (efmesh — 0.2.x, Effect v4 — beta, версия effect пинована peer-зависимостью).
54
62
 
55
63
  **Не для вас**, если нужны: Node-рантайм (пока только Bun), мульти-диалект
56
64
  или облачные DWH (Snowflake/BigQuery — вне scope), стабильность уровня 1.0
@@ -62,7 +70,7 @@ dbt/sqlmesh-подход поверх DuckDB или Postgres и готовы ж
62
70
 
63
71
  **Цели материализации.** Нативная таблица движка, `parquet` (озеро: локально или s3://, интервал = партиция, view поверх `read_parquet`), `ducklake` (таблица-на-fingerprint в [DuckLake](https://ducklake.select)-каталоге — снапшоты и time travel каталога бонусом).
64
72
 
65
- **План и версии.** Fingerprint по каноническому AST (переформатирование SQL не триггерит пересборку), категоризация изменений breaking / non-breaking / indirect / forward-only, `--forward-only` — правка без переигрывания истории (новая версия наследует физику и done-интервалы, новые колонки — `ALTER`), подтверждение плана в TTY, журнал применений с `applied_by`.
73
+ **План и версии.** Fingerprint по каноническому AST (переформатирование SQL не триггерит пересборку), категоризация изменений breaking / non-breaking / indirect / forward-only с обоснованием `plan --explain` и оператором-override `--reclassify`, indirect-реюз физики (потомки non-breaking-правки не пересобираются — scdType2 не теряет историю), `--forward-only` — правка без переигрывания истории (новая версия наследует физику и done-интервалы, новые колонки — `ALTER`), подтверждение плана в TTY, журнал применений с `applied_by`.
66
74
 
67
75
  **Качество.** Контракт схемы перед сборкой (`DESCRIBE` запроса против объявленной Schema), аудиты `notNull` / `unique` / `accepted` (blocking роняет apply, `warn` — логирует), автономный `efmesh audit` по view-слою окружения, `testModel` — юнит-тесты моделей на фикстурах в in-memory DuckDB.
68
76
 
@@ -73,7 +81,7 @@ dbt/sqlmesh-подход поверх DuckDB или Postgres и готовы ж
73
81
  ## Быстрый старт
74
82
 
75
83
  ```sh
76
- bun add -d @avytheone/efmesh@beta
84
+ bun add -d @avytheone/efmesh
77
85
  bunx efmesh init my-warehouse && cd my-warehouse
78
86
  bunx efmesh plan dev # что будет сделано
79
87
  bunx efmesh apply dev # физика, бэкфилл, view-слой
@@ -161,21 +169,88 @@ export default defineConfig({
161
169
  | `efmesh plan <env>` | diff проекта против окружения + недостающие интервалы, ничего не меняет |
162
170
  | `efmesh apply <env>` | план → подтверждение (TTY) → физика, бэкфилл, view-слой |
163
171
  | `efmesh run <env>` | тик планировщика: только новые интервалы, под локом; для cron |
172
+ | `efmesh status <env>` | что происходит: последний план, отставание интервалов, тики run |
164
173
  | `efmesh audit <env>` | аудиты view-слоя окружения — ловит деградацию задним числом |
165
- | `efmesh diff <envA> <envB>` | чем окружения отличаются |
174
+ | `efmesh diff <envA> <envB>` | чем окружения отличаются; `--data` сравнивает сами данные |
166
175
  | `efmesh render <model> [--env]` | итоговый SQL модели |
167
176
  | `efmesh lineage <model[.col]>` | колоночный lineage до сырья |
168
177
  | `efmesh graph [--html]` | DAG моделей текстом или страницей |
169
178
  | `efmesh janitor [--ttl 7]` | снести осиротевшую физику старше ttl |
170
179
  | `efmesh migrate` | догнать схему state store до текущей версии |
180
+ | `efmesh schedule <env>` | зарегистрировать `run <env>` в OS-шедулере через `Bun.cron` |
171
181
 
172
182
  Флаги `apply`/`run`: `--jobs N` — DAG-конкурентность (на DuckDB всегда 1 — одно соединение), `--retries N` — ретраи транзиентных сбоев батча (экспоненциальная пауза), `--yes`/`-y` — без подтверждения, `--forward-only <model>,…` — реюз физики и истории.
173
183
 
184
+ `plan`/`apply` принимают `--reclassify <model>=breaking|non-breaking[,…]` —
185
+ вердикт оператора поверх `--explain`, журналируется с `applied_by`.
186
+ Non-breaking-родитель разрешает нетронутым потомкам реюзать физику вместо
187
+ пересборки (scdType2 не теряет историю строк); override, очевидно
188
+ противоречащий AST (удалённые колонки), не принимается.
189
+
190
+ `plan`, `audit` и `status` принимают `--json` — стабильная машиночитаемая
191
+ форма (контракт в рамках semver) для CI и ботов; exit-коды не меняются,
192
+ stdout — чистый JSON.
193
+
194
+ `plan --explain` добавляет к каждому изменению обоснование: какие узлы
195
+ канонического AST разошлись (`where_clause`, `select_list[2] (добавлен)`, …)
196
+ и почему категория именно такая — включая источники каскада у `indirect`.
197
+ Те же данные попадают в `--json` полем `explain`; пути по AST — отладочная
198
+ подсказка, не часть контракта.
199
+
200
+ `diff <envA> <envB> --data` сравнивает сами данные двух окружений: счётчики
201
+ строк, пересечение по ключу (grain или ключ вида), доли расхождений по
202
+ колонкам среди сопоставленных ключей, дрейф схемы между сторонами.
203
+ `--sample P` (1–99) сравнивает детерминированную долю ключей — md5-бакеты
204
+ выровнены между сторонами, выборка не рождает ложных only-in. `--model a,b`
205
+ сужает, `--json` для CI.
206
+
207
+ `schedule <env> [--cron '@hourly']` регистрирует тик `run` в OS-шедулере
208
+ (crontab / launchd / Task Scheduler) через `Bun.cron` — идемпотентно по
209
+ заголовку, `--remove` снимает, `--list` показывает записи. Честные
210
+ оговорки: OS-cron живёт в локальной таймзоне и не догоняет пропущенные
211
+ запуски, а Arch-семейство Linux вовсе не ставит cron-демона —
212
+ `--print-systemd` печатает user-юниты (`Persistent=true` догоняет).
213
+ Наложение тиков безопасно по построению: `run` берёт env-лок и выходит
214
+ с кодом `2`, когда изменения ждут человека.
215
+
174
216
  Exit-коды: `0` — успех, `1` — ошибка, `2` — «ждёт человека»: план требует
175
217
  подтверждения в не-TTY (нужен `--yes`) или `run` упёрся в неприменённые
176
218
  изменения. В не-TTY `apply` с изменениями БЕЗ `--yes` не применяется —
177
219
  молча катить план, который никто не видел, efmesh отказывается.
178
220
 
221
+ ## Логирование
222
+
223
+ `apply` и `run` рассказывают, что делают. Логи идут в **stderr** — stdout
224
+ остаётся под план, сводки и `--json`, который остаётся байт-чистым JSON.
225
+ Уровни задаёт встроенный флаг `--log-level` (минимальный уровень, по
226
+ умолчанию `info`):
227
+
228
+ - **info** — жизненный цикл, за которым следит человек: старт/финиш сборки
229
+ каждой модели с длительностью, прогресс бэкфила (`batch 3 of 7` с границами
230
+ интервала), промоушен.
231
+ - **warn** — warn-аудиты (нарушения, которые не блокируют) и ретраи.
232
+ - **debug** — рендер SQL перед выполнением, взятие/освобождение лока и прочие
233
+ внутренности. `--log-level debug` также печатает полный fiber-trace при сбое.
234
+
235
+ В каждой строке структурные поля-аннотации (`model`, `env`, `interval`, …). В
236
+ TTY вывод красивый и цветной; в файл или systemd journal — однострочный
237
+ [logfmt](https://brandur.org/logfmt) без ANSI, чтобы читатель логов (или
238
+ ИИ-агент, разбирающий трёхчасовой ночной тик) группировал по полям.
239
+
240
+ Встраиваете efmesh как библиотеку? Логирование — это `Effect.log*`: дайте свой
241
+ `Logger`-слой (приёмник, формат, минимальный уровень), и выбор CLI не действует.
242
+ Число строк не логируется: efmesh не делает лишний запрос ради подсчёта.
243
+
244
+ ## Перфоманс
245
+
246
+ Оверхед фреймворка пренебрежим для любого реалистичного проекта (in-memory DuckDB, `bun bench/plan-bench.ts N`):
247
+
248
+ | моделей | plan холодный | apply (вся физика) | plan пустой | промоушен prod |
249
+ |---|---|---|---|---|
250
+ | 100 | 54 мс | 158 мс | 3 мс | 51 мс |
251
+ | 500 | 228 мс | 759 мс | 11 мс | 197 мс |
252
+ | 2000 | 0.9 с | 2.9 с | 50 мс | 1.3 с |
253
+
179
254
  ## Postgres
180
255
 
181
256
  ```ts