ripple-sql 0.1.0__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 (73) hide show
  1. ripple_sql-0.1.0/.gitignore +31 -0
  2. ripple_sql-0.1.0/CHANGELOG.md +226 -0
  3. ripple_sql-0.1.0/LICENSE +202 -0
  4. ripple_sql-0.1.0/PKG-INFO +285 -0
  5. ripple_sql-0.1.0/README.md +245 -0
  6. ripple_sql-0.1.0/pyproject.toml +79 -0
  7. ripple_sql-0.1.0/src/ripple/__init__.py +31 -0
  8. ripple_sql-0.1.0/src/ripple/answer.py +473 -0
  9. ripple_sql-0.1.0/src/ripple/answer_page.py +214 -0
  10. ripple_sql-0.1.0/src/ripple/cache.py +80 -0
  11. ripple_sql-0.1.0/src/ripple/ci.py +422 -0
  12. ripple_sql-0.1.0/src/ripple/ci_signature.py +374 -0
  13. ripple_sql-0.1.0/src/ripple/cli.py +733 -0
  14. ripple_sql-0.1.0/src/ripple/doctor.py +225 -0
  15. ripple_sql-0.1.0/src/ripple/engine/__init__.py +111 -0
  16. ripple_sql-0.1.0/src/ripple/engine/budget.py +86 -0
  17. ripple_sql-0.1.0/src/ripple/engine/column_lineage.py +112 -0
  18. ripple_sql-0.1.0/src/ripple/engine/column_ref.py +818 -0
  19. ripple_sql-0.1.0/src/ripple/engine/cte_tracing.py +1309 -0
  20. ripple_sql-0.1.0/src/ripple/engine/dependencies.py +466 -0
  21. ripple_sql-0.1.0/src/ripple/engine/dialect.py +132 -0
  22. ripple_sql-0.1.0/src/ripple/engine/dispatch.py +12 -0
  23. ripple_sql-0.1.0/src/ripple/engine/extraction.py +27 -0
  24. ripple_sql-0.1.0/src/ripple/engine/jinja.py +282 -0
  25. ripple_sql-0.1.0/src/ripple/engine/json_sources.py +241 -0
  26. ripple_sql-0.1.0/src/ripple/engine/macro_source.py +127 -0
  27. ripple_sql-0.1.0/src/ripple/engine/pipeline.py +265 -0
  28. ripple_sql-0.1.0/src/ripple/engine/preprocess.py +174 -0
  29. ripple_sql-0.1.0/src/ripple/engine/safe_gen.py +21 -0
  30. ripple_sql-0.1.0/src/ripple/engine/schema_qualification.py +151 -0
  31. ripple_sql-0.1.0/src/ripple/engine/scope.py +488 -0
  32. ripple_sql-0.1.0/src/ripple/engine/select_sources.py +1038 -0
  33. ripple_sql-0.1.0/src/ripple/engine/sql_script.py +729 -0
  34. ripple_sql-0.1.0/src/ripple/engine/statement.py +449 -0
  35. ripple_sql-0.1.0/src/ripple/engine/tech_debt.py +169 -0
  36. ripple_sql-0.1.0/src/ripple/engine/tsql_catalog.py +83 -0
  37. ripple_sql-0.1.0/src/ripple/engine/tsql_scalar_vars.py +248 -0
  38. ripple_sql-0.1.0/src/ripple/engine/tsql_tvf.py +653 -0
  39. ripple_sql-0.1.0/src/ripple/engine/tsql_xml.py +97 -0
  40. ripple_sql-0.1.0/src/ripple/engine/types.py +167 -0
  41. ripple_sql-0.1.0/src/ripple/engine/unused_deps.py +555 -0
  42. ripple_sql-0.1.0/src/ripple/engine/validation.py +158 -0
  43. ripple_sql-0.1.0/src/ripple/graph.py +1499 -0
  44. ripple_sql-0.1.0/src/ripple/home.py +232 -0
  45. ripple_sql-0.1.0/src/ripple/loaders/__init__.py +7 -0
  46. ripple_sql-0.1.0/src/ripple/loaders/dbt.py +359 -0
  47. ripple_sql-0.1.0/src/ripple/loaders/dbt_config.py +339 -0
  48. ripple_sql-0.1.0/src/ripple/loaders/identity.py +328 -0
  49. ripple_sql-0.1.0/src/ripple/loaders/sidecar.py +65 -0
  50. ripple_sql-0.1.0/src/ripple/loaders/sqldir.py +262 -0
  51. ripple_sql-0.1.0/src/ripple/loaders/types.py +197 -0
  52. ripple_sql-0.1.0/src/ripple/lookml.py +163 -0
  53. ripple_sql-0.1.0/src/ripple/mcp_server.py +600 -0
  54. ripple_sql-0.1.0/src/ripple/names.py +40 -0
  55. ripple_sql-0.1.0/src/ripple/project.py +167 -0
  56. ripple_sql-0.1.0/src/ripple/py.typed +0 -0
  57. ripple_sql-0.1.0/src/ripple/render.py +426 -0
  58. ripple_sql-0.1.0/src/ripple/render_shims.py +209 -0
  59. ripple_sql-0.1.0/src/ripple/schemas.py +155 -0
  60. ripple_sql-0.1.0/src/ripple/semantic.py +232 -0
  61. ripple_sql-0.1.0/src/ripple/server.py +184 -0
  62. ripple_sql-0.1.0/src/ripple/sourcefiles.py +64 -0
  63. ripple_sql-0.1.0/src/ripple/star_resolution.py +100 -0
  64. ripple_sql-0.1.0/src/ripple/static/answer.css +146 -0
  65. ripple_sql-0.1.0/src/ripple/static/answer.html +358 -0
  66. ripple_sql-0.1.0/src/ripple/static/answer_twin.js +299 -0
  67. ripple_sql-0.1.0/src/ripple/static/explore.js +133 -0
  68. ripple_sql-0.1.0/src/ripple/usage/__init__.py +18 -0
  69. ripple_sql-0.1.0/src/ripple/usage/cli.py +78 -0
  70. ripple_sql-0.1.0/src/ripple/usage/collect.py +315 -0
  71. ripple_sql-0.1.0/src/ripple/usage/discover.py +190 -0
  72. ripple_sql-0.1.0/src/ripple/usage/ingest.py +414 -0
  73. ripple_sql-0.1.0/src/ripple/usage/report.py +131 -0
@@ -0,0 +1,31 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .pytest_cache/
7
+ .ruff_cache/
8
+ .mypy_cache/
9
+ target/
10
+ .coverage
11
+ htmlcov/
12
+ .env
13
+ .DS_Store
14
+ .idea/
15
+ .vscode/
16
+
17
+ # keep uv.lock tracked (reproducible installs); do not ignore it
18
+ benchmark/corpus/
19
+
20
+ # run logs are local receipts, never tracked
21
+ benchmark/*.log
22
+ coverage_stderr.log
23
+
24
+ benchmark/accuracy_partial.json
25
+
26
+ benchmark/corpus_holdout/*/
27
+ !benchmark/corpus_holdout/manifest.json
28
+
29
+ # private review handoff, never ship
30
+ REVIEW-HANDOFF.md
31
+ PLAN-PRIVATE.md
@@ -0,0 +1,226 @@
1
+ # Changelog
2
+
3
+ Notable changes to Ripple. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and Ripple aims for
5
+ [semantic versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2026-09-08
10
+
11
+ First public release.
12
+
13
+ ### Added
14
+ - Column-level SQL lineage, offline, across Snowflake, BigQuery, Databricks, Redshift, Postgres, DuckDB, and Spark SQL.
15
+ - Three ways in: a dbt project (compiled manifest or raw), a monorepo of dbt projects each parsed in its own dialect, or a plain folder of `.sql` files.
16
+ - `breaks`, `trace`, `models`, `columns`, `graph`, `ci`, `serve`, and `mcp` commands.
17
+ - A trust label on every edge (`verified`, `high_confidence`, `moderate`, `review_required`). Uncertain lineage is flagged, never guessed, and trust follows the path.
18
+ - Multi-statement scripts and dumps handled statement by statement, so a data dump becomes its tables and views instead of a parse error.
19
+ - Downstream reach into the dbt semantic layer (metrics, dimensions) and Looker (LookML dashboards and fields), so a column traces to the metric and dashboard it feeds. Looker support is the `looker` extra.
20
+ - An MCP server, so an agent can ask what a change breaks before it opens a pull request.
21
+ - A GitHub Action and `ripple ci` for pull-request blast-radius comments.
22
+ - `ripple serve` opens on the map: every model in the project in its layer, source tables on the left, dashboards on the right, with the links between them. Hovering a model lights its whole chain upstream and down and fades the rest; clicking one unfolds its columns; clicking a column asks. Typing in the question box filters the map before it asks. Models in a layer sit level with the models feeding them, so links cross as little as one pass can manage. Past forty models a layer folds behind "more", and past four hundred links the wires draw only for the lit chain, so a large repo stays readable. The map is computed in `home.model_map` from the graph's nodes and edges, pure and tested; the page draws it from `static/explore.js` and never fetches. An answer replaces the map with "All models" as the way back. The answer page's stylesheet moved to `static/answer.css`.
23
+ - Two more doors to the answer page. `ripple ci --html PATH` writes one page for the whole change, a view per changed column with the rebuild list and the graph embedded for follow-ups; the GitHub Action keeps it as a workflow artifact and links it from the comment. The MCP `breaks` and `trace` tools now carry the answer's text for the agent to relay, and with `page: true` return `html_path`, a file the person can open. Nothing is sent anywhere.
24
+ - The text answer, rewritten after a readability review: columns that share a source share one line, so the arrow only appears where a name changes; "step" replaces "hop"; dashboard numbers sit in their own section with the kind as a word after the name ("orders (semantic model): is_food_order"); a column the graph could not prove keeps its own line ending in "needs review". jaffle-shop's `ecom.raw_products.type` went from 34 lines to 16. The answer page's JavaScript twin of this text is checked against the Python under node in the test suite.
25
+ - The answer page. `ripple breaks orders.order_total --open` (and `trace`) writes one self-contained HTML file and opens it: the question, the ripple hop by hop with review flags and dashboard numbers marked, the terminal text beside it with a copy button, and a box to ask the next question, answered inside the page from the graph it carries (under a 5 MB budget, `RIPPLE_EMBED_GRAPH_BYTES`). `--html PATH` writes it where you say. No server, nothing sent, nothing loaded from the network. A link with `?q=model.column&dir=trace` opens straight on a question.
26
+ - One answer contract, `ripple.answer`: a `breaks` or `trace` result as nodes with hop depth and trust, links, and summary counts, plus the headline every surface prints. The CLI headline and "hit hardest" line now come from it, so the terminal, MCP, the pull-request comment, and the coming answer page render the same answer.
27
+
28
+ - `benchmark/walk.py` walks the MCP ladder on every corpus and holdout
29
+ repository the way an agent would (summary, unresolved tables, a question
30
+ about each, ingest their columns, ask again, name every model) and fails
31
+ on anything a first-time user would hit. It runs nightly. The first passes
32
+ found seven classes no unit test had, each now pinned: dbt-disabled models
33
+ the summary counted but no tool could name; a monorepo source declared
34
+ twice that took ingested columns on one copy; an ingest swallowed by a
35
+ same-named model; a table created `LIKE` another that accepted any column;
36
+ a model's own name refused as ambiguous; deploy-time placeholder
37
+ qualifiers (`{hscic}.ccgs`, `{{params.dataset}}.blocks`) listed in a
38
+ spelling no ingest could use; and a column list an agent would copy,
39
+ truncated at 200 on a table read on more.
40
+ - `breaks` answers carry `also_matches` when a spelling was settled by being
41
+ one model's own name and other models carry it as an alias.
42
+ - Every MCP response now ends with a `next` line an agent can act on, the
43
+ way the CLI ends on `try:`. The summary and `breaks` hints count before
44
+ they blame: review links that trace to tables outside the project are sent
45
+ to `unresolved_tables`, links reading columns a declared schema lacks say
46
+ which table to re-ingest, and the rest are named as ambiguous names or
47
+ unparsed statements, which no ingest fixes. `ingest_schema` reports the
48
+ review and star-only counts before and after, including an honest
49
+ "nothing moved".
50
+ - `breaks` on a table outside the project answers with the models that read
51
+ it; a column it never saw read returns the columns readers actually use
52
+ (the full list, so it can be handed straight to `ingest_schema`) instead of
53
+ "0 impacted, complete".
54
+ - `unresolved_tables` reports the qualified spelling the SQL wrote (the one an
55
+ ingest must use) and includes tables only the engine saw, such as a raw
56
+ table read by name inside a dbt model.
57
+ - dbt seeds are loaded in raw mode: a CSV's header row is the table's
58
+ columns, so `ref('state_codes')` resolves and `SELECT *` over it expands.
59
+
60
+ - Dialect is read from the SQL when nothing declares it: a file starting
61
+ with `-- Databricks notebook source` is databricks (dbdemos-notebooks was
62
+ parsed as tsql), stl_/svv_ system tables and DISTKEY/SORTKEY are redshift
63
+ (dbt-labs/redshift has no profile and was guessed snowflake), and a dbt
64
+ project with no profile now sniffs its models before falling back to the
65
+ default, saying "the SQL reads as X" rather than "guessed". Plain SQL
66
+ beside a dbt project inherits the project's declared dialect instead of
67
+ assuming postgres (Snowflake's tasty_bytes demo).
68
+ - The summary prints three shared-name warnings and a count instead of
69
+ every one (HTTP Archive's almanac printed 841 lines); `ripple --json` now
70
+ carries the full `warnings` list. The "try:" hint prefers a name a person
71
+ can type over a path-qualified collision name.
72
+ - Plain SQL folders laid out as `<project>/<dataset>/<table>/query.sql`
73
+ (Mozilla's bigquery-etl, Dataform-style repos) now name each relation by
74
+ its directory and answer to `dataset.table` and `project.dataset.table`.
75
+ Before, 887 files were all called "query" and no reference could bind.
76
+ - A directory holding a `schema.yaml`/`schema.json` and no SQL becomes a
77
+ base table with those declared columns, so `SELECT *` views over tables
78
+ loaded outside the repo expand instead of degrading everything downstream
79
+ to review. On bigquery-etl this took trusted links from 6,191 to 22,916.
80
+ - An ambiguous target now suggests the spellings a person would type
81
+ (`telemetry_derived.events_v1`), not path-mangled internal names.
82
+ - `breaks` and `trace` take `--depth N` (and the MCP tools a `depth`
83
+ argument) to follow lineage past the default 25 hops; `trace` now
84
+ discloses when it stopped at the cap, the way `breaks` always has.
85
+ - The per-file wall-clock budget (`RIPPLE_PARSE_BUDGET_S`) now covers the
86
+ analysis phase too, not just the parse: a generated 3.7MB one-line SELECT
87
+ parses in seconds and then wedged column analysis for over two minutes.
88
+ A model that blows the budget reports `timed_out`; the rest of the
89
+ project still resolves.
90
+ - Statements over 1MB are refused up front with the file named and
91
+ `RIPPLE_MAX_SQL_BYTES` to override, instead of grinding for minutes: a
92
+ 10,000-model repo with two multi-MB generated dumps now cold-builds in
93
+ 43s instead of 8+ minutes.
94
+ - Usage ingest: `ripple ingest-usage <file>` takes a query-history export
95
+ (JSONL or JSON array) and `ripple usage` reports which models actually ran
96
+ in the window, which didn't, and what the queries read outside the project.
97
+ Statements with no database context are skipped rather than guessed, the
98
+ word "unused" never appears (a quarterly job looks identical to a dead one),
99
+ and the query text itself is never stored.
100
+ - Usage collect: `ripple collect-usage` (and the `collect_usage` MCP tool)
101
+ runs your already-authenticated warehouse CLI — `snow`, `bq`, or
102
+ `databricks` — and ingests the result in one step. No credential is read,
103
+ stored, or transmitted; the raw rows are spooled to a temp file, aggregated,
104
+ and deleted inside the same call. A partial or polluted export is refused
105
+ outright, and with several configured connections and no declared default,
106
+ collect lists them instead of picking one.
107
+ - MCP: `ingest_usage`, `usage_summary`, and `collect_usage` tools; `ripple
108
+ doctor` self-test for MCP client setup.
109
+ - `ripple ci --select dbt` prints the models to rebuild as a dbt `--select`
110
+ string, computed from git and column lineage: no state backend, no
111
+ manifest artifact, no meter. Enumerates only what the diff actually
112
+ touches; anything uncertain (unparseable diff, depth-capped blast, changed
113
+ row filter, a deleted model's readers) widens to `model+` rather than
114
+ being skipped, so a missed rebuild can't happen silently. The PR comment
115
+ and `--json` report carry the same rebuild set, and `breaks_all` now
116
+ reports honest completeness instead of a hardcoded `true`.
117
+
118
+ ### Changed
119
+ - The answer page looks like its name. A cool water palette: an off-white ground with a hint of blue, deep slate ink, one teal that marks the column you asked about (the drop, with two faint rings that settle when the page opens) and the one button. Links leaving that column carry the teal and fade to gray with distance. A three-ring mark sits by the name. Less to read: the coverage ladder is a status line per rung with the sentence behind a details link, the command's text is folded under a Show text button with Copy text beside it, and the fine print under the question box is one short line.
120
+ - The answer page and `ripple serve` wear a plainer skin: one type scale, one corner radius, system faces, hairlines instead of cards, the accent kept for the one action, state colors kept apart from it (ink for the column you asked about, teal for a dashboard number, amber for a link that needs review), and the command's text on a light well instead of a dark panel. The dotted background, the tracked all-caps labels, and the pill chips are gone.
121
+ - `ripple serve` is now the answer page, live. It opens on a question box, five columns worth asking about first, and the coverage ladder (which external tables still block links, whether query history has been brought in, and the command that moves each rung) instead of painting the whole graph. Answers are computed per request from the files as they are now, by the same renderer the CLI uses, so the page and the terminal cannot disagree. `ripple serve orders.order_total` opens straight on a question. The server stays localhost-only and read-only. `/api/breaks` and `/api/trace` return the answer contract with its text; `/api/graph` is unchanged.
122
+
123
+ ### Fixed
124
+ - Ripple holds on Windows. Every SQL, YAML, LookML, manifest, and cache file is read and written as UTF-8 whatever the machine's code page (a test scans the source for a text read or write that forgets to say so). A pipe out of `ripple` carries UTF-8 too, so the arrows in the text survive `ripple breaks ... > out.txt` and the MCP transport on Windows, and a console that cannot draw an arrow shows a placeholder instead of crashing. A model's path is the same forward-slash string on every OS, so a PR comment written on Linux names the same model on a Windows laptop. CI runs the suite on Windows.
125
+ - Eight shapes `ripple ci` used to miss, all of them edits that change which rows survive while every per-column signature stays the same: a filter reached through a join alias (`join flags f ... where f.big`), a filter reached through two CTE hops, a GROUP BY key or expression, a QUALIFY on the select's own alias, and a union branch. One rule replaces the case-by-case checks: the row-deciding closure, every WHERE, HAVING, QUALIFY, JOIN, GROUP BY, and DISTINCT clause plus the whole chain of expressions behind every column those clauses read, through CTEs, derived tables, and set operations. A change anywhere in it widens to the whole model; when sqlglot cannot resolve the statement the raw clause text is the floor. The closure also covers the clauses that pick which rows survive rather than filtering them: an ORDER BY under a row cap (`limit`, `offset`, `fetch`, `top`) or a `DISTINCT ON`, and every projected expression under a plain `SELECT DISTINCT`, since those decide which rows count as duplicates. Ordering without a cap decides nothing and stays precise, and a projection inside a joined subquery no longer widens readers of untouched columns, while the join's kind, source, match condition, and join condition still do. The set operator itself is part of it, since `UNION` keeps one row per duplicate where `UNION ALL` keeps both and `EXCEPT` is not `INTERSECT`, and so are the clauses that reshape a row set without filtering a column (`TABLESAMPLE`, `CONNECT BY`, `PIVOT`, `MATCH_RECOGNIZE`). Duplicates are compared by value, so renaming an output under `SELECT DISTINCT` decides nothing.
126
+ - A column read only by a downstream WHERE, JOIN, or window key changes every row of that reader. The comment said "nothing downstream" and `--fail-on breaks` let it through; the comment now says "filters rows in paid" (or joins, or frames a window) and the gate fails.
127
+ - `ripple ci --html` crashed with a KeyError on any changed column whose blast radius passed the depth cap, and the change page flattened a window key or a review flag into "filters rows on it". Both facts now travel from ci.py into the page.
128
+ - `ripple breaks --json`, `ripple trace --json`, and the MCP `breaks` and `trace` tools returned the raw graph counts next to the contract's text, so a payload could disagree with its own words. Every JSON door now carries the contract under `answer`, the preview text under `text`, and top-level counts aligned to it. The MCP `page` flag is now inside the tool's declared properties.
129
+ - The answer page's own follow-up walk counted dashboard numbers as columns and skipped wildcard links; it is now a port of the graph's walk, checked against Python under node. A dashboard-only hit no longer reads as "Nothing downstream reads it".
130
+ - A trace stand-in for a reader the walk never listed sat one step further back instead of one step nearer. The contract's merge order now knows the graph's fourth trust level, "moderate", instead of raising on it.
131
+ - `ripple ci --select dbt` no longer lists `metric:`, `semantic:`, or `exposure:` names, which dbt cannot build.
132
+ - `ripple serve` refuses requests whose Host header is not loopback, so a page on another domain that rebinds DNS to 127.0.0.1 cannot read the graph. YAML and LookML edits now refresh the live graph and the disk cache. Concurrent first requests build the graph once.
133
+ - One pruned walk decides which files on disk feed lineage, shared by the disk cache and the live server, so a `dbt run` writing under `target/` no longer invalidates the graph on every request; the live server computes the fingerprint before taking its lock.
134
+ - A follow-up asked on the answer page resolves a model spelling the way the command does, whatever its case, so a name `ripple breaks` answers is no longer refused by the page it wrote. A spelling that names several models says so instead of guessing, and keeps what else it could have meant. A column no link touches is still a column, so `select 1 as x` answers there too. A link that resolves to nothing says so rather than showing the answer the page was written for.
135
+ - The live server saw a file rename that kept the file count and timestamps as no change and answered from a stale graph. One stamp over path, size, and time now serves the live check and the disk cache, and build directories are excluded whatever their case.
136
+ - The answer page's export carries each link's reason once, so an offline follow-up keeps the explanation behind a review flag; equal-depth hits are ordered by depth, trust label, and model on both sides, so the offline text matches the command's; the twin says "at the depth cap" when no depth is known.
137
+ - The home page's starters rank by reader models, not dashboard entries.
138
+ - The `breaks` headline and the pull-request comment counted dashboard numbers as columns: "19 columns in 4 models and 7 dashboard numbers" was 12 columns plus the 7 numbers themselves. One helper now counts model columns and dashboard numbers apart, in the CLI, the MCP payload, the answer page, and the CI comment. A change that reaches only dashboard numbers still fails `--fail-on breaks`.
139
+
140
+ - Ingested schemas were invisible to a dbt project living in a subfolder:
141
+ the CLI and MCP write `.ripple/schemas.json` at the repo root, the loader
142
+ read only the dbt folder. Every ingest on such a repo was a silent no-op.
143
+ - A read of a column missing from a declared or ingested schema was
144
+ downgraded with no reason attached; the edge now says which column and
145
+ which table's list may be incomplete.
146
+ - An ingested qualified table no longer claims a bare name a model owns, and
147
+ `model_columns` prefers the analyzed model when a name is shared with a
148
+ source.
149
+ - The semantic layer parser reads dbt's inline syntax (`semantic_model:`
150
+ nested under a model, dimensions and entities on columns, metrics on the
151
+ model). jaffle-shop migrated to it upstream and the old top-level parser
152
+ silently produced zero semantic nodes, so the README's own hero output
153
+ stopped reproducing. A metric with a constant or computed `expr` is
154
+ dropped, never guessed. The rename now also reaches `metric:lifetime_spend`
155
+ four hops down.
156
+ - Review sweep (11 findings fixed, each pinned by a test):
157
+ a hostile BigQuery `region` value can no longer smuggle SQL into the
158
+ user's authenticated `bq` session; Databricks collection reports its
159
+ scope as `visible-to-this-login` (its history API has no mine/account
160
+ split); a Snowflake window clamped to INFORMATION_SCHEMA's 7 days is
161
+ disclosed instead of silently misreported, and account-scope collections
162
+ hitting the row limit flag truncation; a qualified table read inside a
163
+ same-named CTE is no longer erased; two tables sharing a leaf name both
164
+ count; a valid export mixing offset-less and offset-bearing timestamps
165
+ no longer crashes ingestion; usage summaries (MCP and CLI) warn when the
166
+ project's models changed since ingest; and `ripple doctor`'s fallback
167
+ command now actually forwards `--path`, so the copy-paste config serves
168
+ the project it printed instead of the client's working directory.
169
+ - Usage attribution now uses model identity instead of bare leaf names.
170
+ With dbt-manifest aliases, a read of `other_db.payments` is an outside
171
+ table, not a sighting of this project's `payments`. Without identity,
172
+ one model name referenced from several databases in a single export is
173
+ refused into the ambiguous bucket rather than merged, because merging
174
+ them is exactly how usage counts end up confidently wrong.
175
+ - The graph cache is written atomically (write-then-rename). A crash mid
176
+ write used to leave a corrupt file at the live path, where the matching
177
+ fingerprint made every later session fail the load and silently pay the
178
+ full rebuild, which on a large repo is exactly the MCP client timeout.
179
+ - The declared sqlglot floor was a promise the tests didn't keep: versions
180
+ below 30.14 parse T-SQL temp tables and UPDATE derivations differently,
181
+ producing silently wrong lineage. The floor is now the tested truth
182
+ (`sqlglot>=30.14,<31`), a CI job runs the full suite against the exact
183
+ floor, and a test asserts sqlglot's license stays MIT so a change inside
184
+ the allowed range fails loudly instead of arriving silently.
185
+ - A CTE named after the model that contains it (dbt's `with X as (...)
186
+ select * from X` house style) no longer downgrades every pass-through
187
+ edge to review_required. The CTE shadows only the model's own name, which
188
+ is not ambiguous; on dbt's jaffle-shop this alone was 17 of 18 review
189
+ flags. A CTE shadowing a different model still downgrades.
190
+ - The summary's "why so many review links" line now counts before it blames:
191
+ external tables are only named as the cause with the number of review
192
+ links they actually account for. `ripple ingest-schema` prints whether the
193
+ review count moved, including an honest "still N".
194
+ - `ripple ci` no longer says "nothing downstream" about a removed column
195
+ whose consumers exist: the current graph has already lost the column and
196
+ its edges, so the blast is now computed against the base state.
197
+ - Usage ingest only counts statements that read or write data. GRANT, SHOW,
198
+ DESCRIBE and other administrative statements no longer register as model
199
+ sightings (a GRANT could make a dead model look alive), and `SHOW TABLES
200
+ IN <schema>` no longer fabricates the schema as an outside table.
201
+ `COPY INTO` now counts as a write.
202
+ - `ripple doctor` shell-quotes the config command it prints, so the
203
+ no-binary-on-PATH fallback (`python -c "..."`) survives copy-paste.
204
+ - A collected usage report names its platform and connection instead of the
205
+ temp spool filename.
206
+
207
+ - Window `PARTITION BY` and `ORDER BY` columns are no longer reported as value
208
+ sources. `lag(mrr) over (partition by customer_id order by date_month)` now
209
+ traces to `mrr` alone. The keys are kept as `window` edges into
210
+ `(window key)`, so `breaks` still reports a model whose window partitions on
211
+ a column you renamed. This was the largest miss in the held-out benchmark;
212
+ see `benchmark/HOLDOUT.md` for what that does and does not let us claim.
213
+ - `ripple ci` widened any edit inside a CTE to the whole model: one renamed
214
+ column in a `with source, renamed select *` model reported "internal
215
+ logic changed: 30 columns in 15 downstream models derive differently".
216
+ Inner expressions are now attributed to the output columns whose lineage
217
+ passes through them, so a rename is a remove and an add, and a formula
218
+ change lands on the columns that derive from it. Only an inner expression
219
+ no output column reaches (a flag used just in a WHERE) still widens the
220
+ whole model.
221
+
222
+ ### Removed
223
+ - The dark whole-graph canvas that `ripple serve` used to open (`static/index.html`). Every surface now draws one answer.
224
+
225
+ [Unreleased]: https://github.com/bteh/ripple/compare/v0.1.0...HEAD
226
+ [0.1.0]: https://github.com/bteh/ripple/releases/tag/v0.1.0
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright 2026 Brian Teh
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.