ripple-sql 0.1.0__tar.gz → 0.1.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 (73) hide show
  1. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/CHANGELOG.md +16 -1
  2. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/PKG-INFO +1 -1
  3. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/pyproject.toml +1 -1
  4. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/cli.py +10 -3
  5. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/home.py +6 -1
  6. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/loaders/dbt.py +5 -2
  7. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/loaders/dbt_config.py +3 -1
  8. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/loaders/sqldir.py +2 -2
  9. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/loaders/types.py +30 -0
  10. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/lookml.py +2 -1
  11. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/project.py +1 -1
  12. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/semantic.py +2 -1
  13. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/static/answer.css +2 -0
  14. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/static/answer.html +13 -3
  15. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/static/explore.js +5 -2
  16. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/.gitignore +0 -0
  17. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/LICENSE +0 -0
  18. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/README.md +0 -0
  19. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/__init__.py +0 -0
  20. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/answer.py +0 -0
  21. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/answer_page.py +0 -0
  22. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/cache.py +0 -0
  23. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/ci.py +0 -0
  24. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/ci_signature.py +0 -0
  25. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/doctor.py +0 -0
  26. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/__init__.py +0 -0
  27. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/budget.py +0 -0
  28. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/column_lineage.py +0 -0
  29. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/column_ref.py +0 -0
  30. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/cte_tracing.py +0 -0
  31. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/dependencies.py +0 -0
  32. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/dialect.py +0 -0
  33. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/dispatch.py +0 -0
  34. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/extraction.py +0 -0
  35. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/jinja.py +0 -0
  36. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/json_sources.py +0 -0
  37. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/macro_source.py +0 -0
  38. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/pipeline.py +0 -0
  39. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/preprocess.py +0 -0
  40. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/safe_gen.py +0 -0
  41. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/schema_qualification.py +0 -0
  42. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/scope.py +0 -0
  43. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/select_sources.py +0 -0
  44. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/sql_script.py +0 -0
  45. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/statement.py +0 -0
  46. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/tech_debt.py +0 -0
  47. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/tsql_catalog.py +0 -0
  48. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/tsql_scalar_vars.py +0 -0
  49. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/tsql_tvf.py +0 -0
  50. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/tsql_xml.py +0 -0
  51. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/types.py +0 -0
  52. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/unused_deps.py +0 -0
  53. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/engine/validation.py +0 -0
  54. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/graph.py +0 -0
  55. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/loaders/__init__.py +0 -0
  56. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/loaders/identity.py +0 -0
  57. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/loaders/sidecar.py +0 -0
  58. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/mcp_server.py +0 -0
  59. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/names.py +0 -0
  60. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/py.typed +0 -0
  61. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/render.py +0 -0
  62. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/render_shims.py +0 -0
  63. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/schemas.py +0 -0
  64. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/server.py +0 -0
  65. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/sourcefiles.py +0 -0
  66. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/star_resolution.py +0 -0
  67. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/static/answer_twin.js +0 -0
  68. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/usage/__init__.py +0 -0
  69. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/usage/cli.py +0 -0
  70. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/usage/collect.py +0 -0
  71. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/usage/discover.py +0 -0
  72. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/usage/ingest.py +0 -0
  73. {ripple_sql-0.1.0 → ripple_sql-0.1.2}/src/ripple/usage/report.py +0 -0
@@ -6,6 +6,19 @@ Notable changes to Ripple. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.1.2] - 2026-09-09
10
+
11
+ ### Fixed
12
+ - A symlink whose target is gone matched the SQL, YAML, LookML, macro, and seed globs and crashed the load with a traceback on the first real folder the published build ran in. One helper, `files_under`, now serves every project glob and keeps regular files only.
13
+
14
+ ## [0.1.1] - 2026-09-09
15
+
16
+ ### Changed
17
+ - The map's home page says what it is for in one line ("Pick a column. See what breaks if it changes, or where it comes from.") and offers one example column to try; the five most-read columns come back with an answer. A source table's chip no longer shows a count, since the number was the columns some model happened to name, not the table; opening one says the list is not declared in the project and points at the Coverage rung that adds it.
18
+
19
+ ### Fixed
20
+ - Run from a home directory or the root of a disk, ripple refuses in one sentence instead of reading every SQL file the user owns. Progress lines ("scanning ...") reach a pipe as they are printed, not when the process exits.
21
+
9
22
  ## [0.1.0] - 2026-09-08
10
23
 
11
24
  First public release.
@@ -222,5 +235,7 @@ First public release.
222
235
  ### Removed
223
236
  - The dark whole-graph canvas that `ripple serve` used to open (`static/index.html`). Every surface now draws one answer.
224
237
 
225
- [Unreleased]: https://github.com/bteh/ripple/compare/v0.1.0...HEAD
238
+ [Unreleased]: https://github.com/bteh/ripple/compare/v0.1.2...HEAD
239
+ [0.1.2]: https://github.com/bteh/ripple/releases/tag/v0.1.2
240
+ [0.1.1]: https://github.com/bteh/ripple/releases/tag/v0.1.1
226
241
  [0.1.0]: https://github.com/bteh/ripple/releases/tag/v0.1.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ripple-sql
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Offline column-level SQL lineage. See what breaks before you merge.
5
5
  Project-URL: Homepage, https://github.com/bteh/ripple
6
6
  Project-URL: Repository, https://github.com/bteh/ripple
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ripple-sql"
3
- version = "0.1.0"
3
+ version = "0.1.2"
4
4
  description = "Offline column-level SQL lineage. See what breaks before you merge."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -570,10 +570,12 @@ def utf8_streams() -> None:
570
570
  for stream in (sys.stdin, sys.stdout, sys.stderr):
571
571
  if stream is None or not hasattr(stream, "reconfigure"):
572
572
  continue
573
+ # line buffered either way: "scanning ..." must show while the scan
574
+ # runs, not when the process exits, even through a pipe
573
575
  if stream.isatty():
574
- stream.reconfigure(errors="replace")
576
+ stream.reconfigure(errors="replace", line_buffering=True)
575
577
  else:
576
- stream.reconfigure(encoding="utf-8", errors="replace")
578
+ stream.reconfigure(encoding="utf-8", errors="replace", line_buffering=True)
577
579
 
578
580
 
579
581
  def main(argv: list[str] | None = None) -> None:
@@ -726,7 +728,12 @@ def main(argv: list[str] | None = None) -> None:
726
728
  "doctor": cmd_doctor,
727
729
  "serve": cmd_serve,
728
730
  }
729
- handlers[args.command](args)
731
+ from ripple.loaders.types import NotAProject
732
+
733
+ try:
734
+ handlers[args.command](args)
735
+ except NotAProject as e:
736
+ sys.exit(str(e))
730
737
 
731
738
 
732
739
  if __name__ == "__main__":
@@ -130,10 +130,14 @@ def model_map(nodes: list[dict], edges) -> dict:
130
130
  members: dict[str, dict] = {}
131
131
  for n in nodes:
132
132
  source = n["type"] == "source"
133
+ columns = [c for c in n.get("columns") or [] if _is_column(c)]
133
134
  members[n["name"]] = {
134
135
  "id": n["name"],
135
136
  "kind": "source" if source else "model",
136
- "columns": [c for c in n.get("columns") or [] if _is_column(c)],
137
+ "columns": columns,
138
+ # a source with no declared list only shows the columns some
139
+ # model happens to name; the page must not present that as the table
140
+ "declared": bool(columns),
137
141
  "status": None if source else n.get("status"),
138
142
  "path": None if source else n.get("path"),
139
143
  }
@@ -156,6 +160,7 @@ def model_map(nodes: list[dict], edges) -> dict:
156
160
  "id": name,
157
161
  "kind": kind,
158
162
  "columns": sorted(seen.get(name, {})),
163
+ "declared": kind == "dashboard",
159
164
  "status": None,
160
165
  "path": None,
161
166
  }
@@ -24,6 +24,7 @@ from ripple.loaders.types import (
24
24
  DEFAULT_DIALECT,
25
25
  Model,
26
26
  Project,
27
+ files_under,
27
28
  relative_posix,
28
29
  )
29
30
 
@@ -124,7 +125,7 @@ def _seed_tables(root: Path) -> list[Model]:
124
125
 
125
126
  seeds: list[Model] = []
126
127
  for seed_dir in _seed_dirs(root):
127
- for path in sorted(seed_dir.rglob("*.csv")):
128
+ for path in files_under(seed_dir, "*.csv"):
128
129
  try:
129
130
  with open(path, newline="", encoding="utf-8", errors="replace") as f:
130
131
  header = next(csv.reader(f), [])
@@ -257,7 +258,9 @@ def _load_dbt_raw(root: Path, dialect: str | None) -> Project:
257
258
  "run 'dbt compile' for exact lineage."
258
259
  ]
259
260
  model_dirs = _dbt_model_dirs(root)
260
- pairs = sorted(((d, p) for d in model_dirs for p in d.rglob("*.sql")), key=lambda t: t[1])
261
+ pairs = sorted(
262
+ ((d, p) for d in model_dirs for p in files_under(d, "*.sql")), key=lambda t: t[1]
263
+ )
261
264
  # a slice of every model, the same sample the sql-dir loader sniffs
262
265
  sample = "\n".join(p.read_text(errors="replace", encoding="utf-8")[:4000] for _, p in pairs)[
263
266
  :200000
@@ -5,6 +5,8 @@ from __future__ import annotations
5
5
  import re
6
6
  from pathlib import Path
7
7
 
8
+ from ripple.loaders.types import files_under
9
+
8
10
 
9
11
  def _project_vars(root: Path) -> dict:
10
12
  """Declared vars from dbt_project.yml. A sub-dict scoped under the
@@ -236,7 +238,7 @@ def _macro_sources(root: Path) -> list[tuple[str | None, str]]:
236
238
  macros_dir = base / dir_name
237
239
  if not macros_dir.is_dir():
238
240
  continue
239
- for macro_file in sorted(macros_dir.rglob("*.sql")):
241
+ for macro_file in files_under(macros_dir, "*.sql"):
240
242
  resolved = macro_file.resolve()
241
243
  if resolved in seen_files:
242
244
  continue
@@ -6,7 +6,7 @@ from pathlib import Path
6
6
 
7
7
  from ripple.engine.budget import TIMED_OUT, budget_seconds, max_sql_bytes, run_within_budget
8
8
  from ripple.loaders.identity import directory_identity
9
- from ripple.loaders.types import IGNORE_DIRS, Model, Project, relative_posix
9
+ from ripple.loaders.types import IGNORE_DIRS, Model, Project, files_under, relative_posix
10
10
 
11
11
 
12
12
  def _iter_sql_files(root: Path, exclude: list[Path] | None = None) -> tuple[list[Path], int]:
@@ -19,7 +19,7 @@ def _iter_sql_files(root: Path, exclude: list[Path] | None = None) -> tuple[list
19
19
  kept: list[Path] = []
20
20
  skipped = 0
21
21
  excluded = [d.resolve() for d in (exclude or [])]
22
- for path in sorted(root.rglob("*.sql")):
22
+ for path in files_under(root, "*.sql"):
23
23
  dirs = path.relative_to(root).parts[:-1]
24
24
  if any(part in IGNORE_DIRS or part.startswith(".") for part in dirs):
25
25
  skipped += 1
@@ -169,6 +169,16 @@ def find_project_root(start: Path) -> Path:
169
169
  for candidate in [current, *current.parents]:
170
170
  if (candidate / "dbt_project.yml").exists():
171
171
  return candidate
172
+ # every caller passes through here before it stamps the cache or reads
173
+ # a file, so this is where a whole machine is refused
174
+ if whole_machine(current):
175
+ where = (
176
+ "your home directory" if current == Path.home().resolve() else "the root of the disk"
177
+ )
178
+ raise NotAProject(
179
+ f"{current} is {where}, not a project; run ripple inside a project folder, "
180
+ "or pass the folder with --path"
181
+ )
172
182
  return current
173
183
 
174
184
 
@@ -195,3 +205,23 @@ def relative_posix(path, root) -> str:
195
205
  OS, so a model's path is the same string in a PR comment written on
196
206
  Linux and on the laptop that reads it."""
197
207
  return path.relative_to(root).as_posix()
208
+
209
+
210
+ class NotAProject(ValueError):
211
+ """The path is a whole machine, not a project: reading it would crawl
212
+ everything under the home directory or the disk."""
213
+
214
+
215
+ def whole_machine(path: Path) -> bool:
216
+ """True for the home directory and for the root of a disk. A plain-SQL
217
+ scan starts wherever the command runs, and from either of those it
218
+ would read every file the user owns."""
219
+ here = Path(path).resolve()
220
+ return here == Path.home().resolve() or here == Path(here.anchor)
221
+
222
+
223
+ def files_under(root: Path, pattern: str) -> list[Path]:
224
+ """The regular files matching a glob under root, sorted. A symlink whose
225
+ target is gone matches the glob and is not a file; reading it raised a
226
+ traceback on the first real folder this ran in."""
227
+ return sorted(p for p in root.rglob(pattern) if p.is_file())
@@ -16,6 +16,7 @@ from pathlib import Path
16
16
 
17
17
  import yaml
18
18
 
19
+ from ripple.loaders.types import files_under
19
20
  from ripple.project import IGNORE_DIRS
20
21
  from ripple.semantic import DownstreamEdge
21
22
 
@@ -133,7 +134,7 @@ def load_lookml_downstream(root: Path) -> list[DownstreamEdge]:
133
134
  return []
134
135
 
135
136
  edges: list[DownstreamEdge] = []
136
- for path in sorted(root.rglob("*.lkml")):
137
+ for path in files_under(root, "*.lkml"):
137
138
  parts = path.relative_to(root).parts[:-1]
138
139
  if any(p in IGNORE_DIRS or p.startswith(".") for p in parts):
139
140
  continue
@@ -49,7 +49,7 @@ __all__ = [
49
49
 
50
50
  def load_project(path: str | Path = ".", dialect: str | None = None) -> Project:
51
51
  start = Path(path).resolve()
52
- root = find_project_root(start)
52
+ root = find_project_root(start) # refuses a home directory or a disk root
53
53
  # If we're standing above several dbt projects (a monorepo), gather them
54
54
  # all. find_project_root only walks UP to the nearest one, so scan DOWN.
55
55
  scan_root = root if (root / "dbt_project.yml").exists() else start
@@ -19,6 +19,7 @@ from pathlib import Path
19
19
 
20
20
  import yaml
21
21
 
22
+ from ripple.loaders.types import files_under
22
23
  from ripple.project import IGNORE_DIRS
23
24
 
24
25
  _REF = re.compile(r"ref\(\s*['\"]([^'\"]+)['\"]\s*(?:,\s*['\"]([^'\"]+)['\"])?\s*\)")
@@ -47,7 +48,7 @@ def _ref_model(value) -> str | None:
47
48
 
48
49
 
49
50
  def _iter_schema_docs(root: Path):
50
- for path in sorted(root.rglob("*.yml")) + sorted(root.rglob("*.yaml")):
51
+ for path in files_under(root, "*.yml") + files_under(root, "*.yaml"):
51
52
  parts = path.relative_to(root).parts[:-1]
52
53
  if any(p in IGNORE_DIRS or p.startswith(".") for p in parts):
53
54
  continue
@@ -24,6 +24,7 @@ h1 code{font:500 24px/1 var(--mono); letter-spacing:0}
24
24
  .sum b{color:var(--ink); font-weight:600}
25
25
  .sum > span{position:relative}
26
26
  .sum > span + span::before{content:""; position:absolute; left:-9px; top:.3em; height:.9em; border-left:1px solid var(--line-strong)}
27
+ .lede{margin:14px 0 0; font-size:16px; color:var(--ink); text-wrap:pretty}
27
28
 
28
29
  .ask{display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin:28px 0 0; padding:16px 0 14px; border-top:1px solid var(--line); border-bottom:1px solid var(--line)}
29
30
  .ask input,.ask select{font:400 14px var(--mono); padding:8px 10px; border:1px solid var(--line-strong); border-radius:var(--r); background:var(--surface); color:var(--ink); min-width:0; height:36px}
@@ -105,6 +106,7 @@ pre{margin:0; padding:16px 22px 18px; overflow-x:auto; font:400 13px/1.65 var(--
105
106
  .cols{transition:opacity .18s var(--ease)}
106
107
  .wires path.on{stroke:var(--accent); stroke-width:1.5}
107
108
  .cols{display:flex; flex-direction:column; gap:5px; margin:0 0 6px 10px; padding-left:10px; border-left:1px solid var(--line)}
109
+ .cols .gname{max-width:36ch; white-space:normal; line-height:1.45; padding-top:2px}
108
110
  .cols .node{cursor:pointer}
109
111
  .cols .node:hover{border-color:var(--accent); color:var(--accent-strong)}
110
112
  .more-chips{font:500 12.5px var(--sans); color:var(--accent); background:none; border:0; padding:4px 0; cursor:pointer; text-align:left}
@@ -13,6 +13,7 @@
13
13
  <p class="eyebrow"><span class="mark" aria-hidden="true"></span><span id="eyebrow">Ripple</span></p>
14
14
  <h1 id="title"></h1>
15
15
  <p class="sum" id="sum"></p>
16
+ <p class="lede" id="lede" hidden>Pick a column. See what breaks if it changes, or where it comes from.</p>
16
17
 
17
18
  <div class="change" id="change" hidden><span class="lab">In this change</span></div>
18
19
 
@@ -77,7 +78,8 @@
77
78
  demo.hidden = false;
78
79
  mapEl.hidden = true;
79
80
  nav.hidden = !mapApi;
80
- if (mapApi && L.starters.length) document.getElementById('starters').hidden = false;
81
+ document.getElementById('lede').hidden = true;
82
+ if (mapApi) starterStrip(true);
81
83
  document.getElementById('eyebrow').textContent = 'Ripple · ' + (DATA.change ? 'what this change breaks' : (a.kind === 'trace' ? 'where it comes from' : 'what breaks'));
82
84
  document.getElementById('title').innerHTML = a.kind === 'trace'
83
85
  ? 'Where does <code>' + esc(a.target) + '</code> come from?'
@@ -185,6 +187,14 @@
185
187
 
186
188
  // ---- the home state: ripple serve opens here, on the whole project ----
187
189
  var P = DATA.project || {}, L = DATA.live;
190
+ // one example on the map ("Try"), the five most-read columns with an answer
191
+ function starterStrip(all){
192
+ var strip = document.getElementById('starters');
193
+ if (!L || !L.starters.length) return;
194
+ strip.hidden = false;
195
+ strip.querySelector('.lab').textContent = all ? 'Start with' : 'Try';
196
+ Array.prototype.slice.call(strip.querySelectorAll('button')).forEach(function(b, i){ b.hidden = !all && i > 0; });
197
+ }
188
198
  function home(){
189
199
  current = null;
190
200
  demo.hidden = true;
@@ -200,9 +210,9 @@
200
210
  if (L.review) bits.push('<b>' + L.review + ' need review</b>');
201
211
  document.getElementById('sum').innerHTML = bits.map(function(b){ return '<span>' + b + '</span>'; }).join('');
202
212
  document.title = 'Ripple: ' + (P.root || 'local');
213
+ document.getElementById('lede').hidden = !mapApi;
203
214
  if (mapApi){
204
- // the map is the place to start; the starter chips come back with an answer
205
- document.getElementById('starters').hidden = true;
215
+ starterStrip(false);
206
216
  mapEl.hidden = false;
207
217
  mapApi.filter(document.getElementById('q').value.trim());
208
218
  requestAnimationFrame(mapApi.draw);
@@ -47,7 +47,8 @@ function renderMap(root, map, opts){
47
47
  var broken = m.status && m.status !== 'ok' && m.status !== 'star_only';
48
48
  var b = el('button', 'mdl' + (m.kind === 'source' ? ' src' : '') + (m.kind === 'dashboard' ? ' bi' : '') + (broken ? ' r' : ''));
49
49
  b.type = 'button'; b.dataset.id = m.id; b.setAttribute('aria-expanded', 'false');
50
- b.innerHTML = label(m) + (m.columns.length ? ' <small>' + m.columns.length + '</small>' : '');
50
+ // a source's count would be the columns some model names, not the table's width
51
+ b.innerHTML = label(m) + (m.declared && m.kind !== 'source' && m.columns.length ? ' <small>' + m.columns.length + '</small>' : '');
51
52
  var reads = (ups[m.id] || []).length, feeds = (downs[m.id] || []).length;
52
53
  b.title = (m.path || m.id) + ' · reads ' + reads + ', feeds ' + feeds + (broken ? ' · ' + m.status.replace('_', ' ') : '');
53
54
  b.addEventListener('click', function(){ toggle(m, g, b); });
@@ -81,7 +82,9 @@ function renderMap(root, map, opts){
81
82
  });
82
83
  var note = null;
83
84
  if (m.columns.length > MAP_COLUMNS) note = '+ ' + (m.columns.length - MAP_COLUMNS) + ' more, ask by name';
84
- else if (!m.columns.length) note = m.kind === 'source' ? 'columns unknown, see Coverage' : 'no columns known';
85
+ else if (!m.declared && m.kind === 'source') note = (m.columns.length ? 'only the columns this project names; the full list is' : 'no column list; it is') +
86
+ ' not declared in the project (a select * reads it unnamed). Coverage below adds it.';
87
+ else if (!m.columns.length) note = 'no columns known';
85
88
  if (note){ var n = el('span', 'gname'); n.textContent = note; cols.appendChild(n); }
86
89
  g.appendChild(cols); b.setAttribute('aria-expanded', 'true'); draw();
87
90
  }
File without changes
File without changes
File without changes
File without changes