solunex-ssdo 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 (92) hide show
  1. solunex_ssdo-0.1.0/PKG-INFO +404 -0
  2. solunex_ssdo-0.1.0/README.md +386 -0
  3. solunex_ssdo-0.1.0/pyproject.toml +36 -0
  4. solunex_ssdo-0.1.0/setup.cfg +4 -0
  5. solunex_ssdo-0.1.0/solunex_ssdo.egg-info/PKG-INFO +404 -0
  6. solunex_ssdo-0.1.0/solunex_ssdo.egg-info/SOURCES.txt +90 -0
  7. solunex_ssdo-0.1.0/solunex_ssdo.egg-info/dependency_links.txt +1 -0
  8. solunex_ssdo-0.1.0/solunex_ssdo.egg-info/entry_points.txt +2 -0
  9. solunex_ssdo-0.1.0/solunex_ssdo.egg-info/requires.txt +11 -0
  10. solunex_ssdo-0.1.0/solunex_ssdo.egg-info/top_level.txt +1 -0
  11. solunex_ssdo-0.1.0/ssdo/__init__.py +2 -0
  12. solunex_ssdo-0.1.0/ssdo/api/__init__.py +0 -0
  13. solunex_ssdo-0.1.0/ssdo/api/auth.py +118 -0
  14. solunex_ssdo-0.1.0/ssdo/api/main.py +1285 -0
  15. solunex_ssdo-0.1.0/ssdo/api/models.py +296 -0
  16. solunex_ssdo-0.1.0/ssdo/api/notify.py +83 -0
  17. solunex_ssdo-0.1.0/ssdo/api/security.py +207 -0
  18. solunex_ssdo-0.1.0/ssdo/api/static/index.html +5309 -0
  19. solunex_ssdo-0.1.0/ssdo/applier.py +94 -0
  20. solunex_ssdo-0.1.0/ssdo/cli.py +187 -0
  21. solunex_ssdo-0.1.0/ssdo/core/__init__.py +25 -0
  22. solunex_ssdo-0.1.0/ssdo/core/appmap.py +453 -0
  23. solunex_ssdo-0.1.0/ssdo/core/ask.py +1507 -0
  24. solunex_ssdo-0.1.0/ssdo/core/ask_pair.py +786 -0
  25. solunex_ssdo-0.1.0/ssdo/core/compare.py +309 -0
  26. solunex_ssdo-0.1.0/ssdo/core/confidence.py +85 -0
  27. solunex_ssdo-0.1.0/ssdo/core/ddl.py +909 -0
  28. solunex_ssdo-0.1.0/ssdo/core/discussion.py +182 -0
  29. solunex_ssdo-0.1.0/ssdo/core/migration.py +571 -0
  30. solunex_ssdo-0.1.0/ssdo/core/naming.py +96 -0
  31. solunex_ssdo-0.1.0/ssdo/core/pair.py +324 -0
  32. solunex_ssdo-0.1.0/ssdo/core/plainlang.py +264 -0
  33. solunex_ssdo-0.1.0/ssdo/core/reasoner.py +113 -0
  34. solunex_ssdo-0.1.0/ssdo/core/render.py +51 -0
  35. solunex_ssdo-0.1.0/ssdo/core/report.py +352 -0
  36. solunex_ssdo-0.1.0/ssdo/core/rules.py +217 -0
  37. solunex_ssdo-0.1.0/ssdo/core/starters.py +74 -0
  38. solunex_ssdo-0.1.0/ssdo/core/structure.py +138 -0
  39. solunex_ssdo-0.1.0/ssdo/core/teaching.py +372 -0
  40. solunex_ssdo-0.1.0/ssdo/core/tuning.py +41 -0
  41. solunex_ssdo-0.1.0/ssdo/core/watch.py +178 -0
  42. solunex_ssdo-0.1.0/ssdo/db/__init__.py +26 -0
  43. solunex_ssdo-0.1.0/ssdo/db/accounts.py +419 -0
  44. solunex_ssdo-0.1.0/ssdo/db/admin.py +137 -0
  45. solunex_ssdo-0.1.0/ssdo/db/applies.py +175 -0
  46. solunex_ssdo-0.1.0/ssdo/db/cards.py +161 -0
  47. solunex_ssdo-0.1.0/ssdo/db/chat.py +179 -0
  48. solunex_ssdo-0.1.0/ssdo/db/config.py +35 -0
  49. solunex_ssdo-0.1.0/ssdo/db/draftdbs.py +131 -0
  50. solunex_ssdo-0.1.0/ssdo/db/drafts.py +81 -0
  51. solunex_ssdo-0.1.0/ssdo/db/join.py +247 -0
  52. solunex_ssdo-0.1.0/ssdo/db/migrate.py +38 -0
  53. solunex_ssdo-0.1.0/ssdo/db/migrations/env.py +100 -0
  54. solunex_ssdo-0.1.0/ssdo/db/migrations/script.py.mako +24 -0
  55. solunex_ssdo-0.1.0/ssdo/db/migrations/versions/0001_baseline.py +93 -0
  56. solunex_ssdo-0.1.0/ssdo/db/migrations/versions/0002_column_inventory.py +51 -0
  57. solunex_ssdo-0.1.0/ssdo/db/migrations/versions/0003_accounts.py +68 -0
  58. solunex_ssdo-0.1.0/ssdo/db/migrations/versions/0004_vault.py +78 -0
  59. solunex_ssdo-0.1.0/ssdo/db/migrations/versions/0005_private_workspaces.py +74 -0
  60. solunex_ssdo-0.1.0/ssdo/db/migrations/versions/0006_live_workspace.py +67 -0
  61. solunex_ssdo-0.1.0/ssdo/db/migrations/versions/0007_feedback.py +38 -0
  62. solunex_ssdo-0.1.0/ssdo/db/migrations/versions/0008_private_watches.py +70 -0
  63. solunex_ssdo-0.1.0/ssdo/db/migrations/versions/0009_live_rooms.py +69 -0
  64. solunex_ssdo-0.1.0/ssdo/db/migrations/versions/0010_usernames.py +62 -0
  65. solunex_ssdo-0.1.0/ssdo/db/migrations/versions/0011_room_chat.py +42 -0
  66. solunex_ssdo-0.1.0/ssdo/db/migrations/versions/0012_connection_cards.py +48 -0
  67. solunex_ssdo-0.1.0/ssdo/db/migrations/versions/0013_draft_databases.py +35 -0
  68. solunex_ssdo-0.1.0/ssdo/db/migrations/versions/0014_enum_values.py +27 -0
  69. solunex_ssdo-0.1.0/ssdo/db/migrations/versions/0015_watch_applies.py +48 -0
  70. solunex_ssdo-0.1.0/ssdo/db/migrations/versions/__init__.py +0 -0
  71. solunex_ssdo-0.1.0/ssdo/db/models.py +468 -0
  72. solunex_ssdo-0.1.0/ssdo/db/rooms.py +500 -0
  73. solunex_ssdo-0.1.0/ssdo/db/seal.py +90 -0
  74. solunex_ssdo-0.1.0/ssdo/db/store.py +298 -0
  75. solunex_ssdo-0.1.0/ssdo/db/vault.py +203 -0
  76. solunex_ssdo-0.1.0/ssdo/db/watches.py +263 -0
  77. solunex_ssdo-0.1.0/ssdo/ghcomment.py +97 -0
  78. solunex_ssdo-0.1.0/ssdo/profiler/__init__.py +13 -0
  79. solunex_ssdo-0.1.0/ssdo/profiler/connection.py +59 -0
  80. solunex_ssdo-0.1.0/ssdo/profiler/mysql_profiler.py +196 -0
  81. solunex_ssdo-0.1.0/ssdo/profiler/postgres_profiler.py +258 -0
  82. solunex_ssdo-0.1.0/ssdo/profiler/tls.py +159 -0
  83. solunex_ssdo-0.1.0/ssdo/schemadiff.py +106 -0
  84. solunex_ssdo-0.1.0/ssdo/schemas/__init__.py +0 -0
  85. solunex_ssdo-0.1.0/ssdo/schemas/profile.py +157 -0
  86. solunex_ssdo-0.1.0/ssdo/watcher.py +282 -0
  87. solunex_ssdo-0.1.0/tests/test_applier_live.py +77 -0
  88. solunex_ssdo-0.1.0/tests/test_cli.py +75 -0
  89. solunex_ssdo-0.1.0/tests/test_ghcomment.py +57 -0
  90. solunex_ssdo-0.1.0/tests/test_schemadiff.py +137 -0
  91. solunex_ssdo-0.1.0/tests/test_watcher.py +111 -0
  92. solunex_ssdo-0.1.0/tests/test_watcher_apply.py +98 -0
@@ -0,0 +1,404 @@
1
+ Metadata-Version: 2.4
2
+ Name: solunex-ssdo
3
+ Version: 0.1.0
4
+ Summary: SSDO, the Smart Data Organizer: understands a database's structure (never its data) and says what it knows, what it infers, and how sure it is.
5
+ Author: Solunex Technologies
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: fastapi>=0.110
9
+ Requires-Dist: uvicorn>=0.29
10
+ Requires-Dist: pydantic>=2.6
11
+ Requires-Dist: SQLAlchemy>=2.0
12
+ Requires-Dist: alembic>=1.13
13
+ Requires-Dist: PyMySQL>=1.1
14
+ Requires-Dist: psycopg[binary]>=3.1
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest; extra == "dev"
17
+ Requires-Dist: httpx; extra == "dev"
18
+
19
+ # SSDO — Smart Data Organizer (Solunex Intelligence)
20
+
21
+ SSDO profiles a MySQL or PostgreSQL database's **structure** (`information_schema`
22
+ only — no data rows are read) and returns an explainable understanding of it:
23
+ relationships (declared and inferred, each with confidence and evidence), table
24
+ roles, conventions, and what it honestly could not determine.
25
+
26
+ ## Run SSDO on your own machine
27
+
28
+ Use this to profile databases on your laptop or office network (`localhost`) in
29
+ development mode, before anything is hosted. It is the same SSDO: structure only, never
30
+ your data. The console opens at http://127.0.0.1:8000 and only this machine can reach it.
31
+
32
+ **With Python (3.10+):**
33
+
34
+ ```bash
35
+ pip install "git+https://github.com/saniabduljabbar619-create/solunex-intelligence.git"
36
+ ssdo # starts SSDO and opens the console in your browser
37
+ ssdo --port 9000 --no-browser # another port, no browser
38
+ ssdo --version
39
+ ```
40
+
41
+ SSDO keeps its saved runs in your user data folder (Windows
42
+ `%LOCALAPPDATA%\Solunex\SSDO`, macOS `~/Library/Application Support/Solunex SSDO`,
43
+ Linux `~/.local/share/solunex-ssdo`); `SSDO_DB_URL` overrides it.
44
+
45
+ **With Docker:**
46
+
47
+ ```bash
48
+ docker build -t solunex-ssdo .
49
+ docker run --rm -p 127.0.0.1:8000:8000 -v ssdo-data:/data solunex-ssdo
50
+ ```
51
+
52
+ - Keep the `127.0.0.1:` in `-p` so only your machine can open the console.
53
+ - Saved runs live in the `ssdo-data` volume.
54
+ - To profile a database running on the same computer, use the host
55
+ **`host.docker.internal`** instead of `localhost`. On Linux, add
56
+ `--add-host=host.docker.internal:host-gateway` to `docker run`.
57
+ - Tagged releases (`vX.Y.Z`) also publish the image to
58
+ `ghcr.io/saniabduljabbar619-create/solunex-ssdo`.
59
+
60
+ ## Setup (to work on SSDO itself)
61
+
62
+ ```bash
63
+ python -m venv .venv
64
+ .venv\Scripts\activate # Windows (macOS/Linux: source .venv/bin/activate)
65
+ pip install -r requirements-dev.txt
66
+ ```
67
+
68
+ ## Run the API
69
+
70
+ ```bash
71
+ uvicorn ssdo.api.main:app --reload
72
+ ```
73
+
74
+ Open http://127.0.0.1:8000 for the console, or http://127.0.0.1:8000/docs for Swagger.
75
+ The console has four views of every run: **Show technical detail**, **Explain simply**
76
+ (plain language), **Teach me** (a six-slide guided walkthrough, also available as
77
+ `GET /v1/understandings/{run_id}/walkthrough`) and **Changes** (drift since an earlier
78
+ run, also `GET /v1/compare` and `GET /v1/understandings/{run_id}/changes`).
79
+
80
+ Above the views, **Ask SSDO** answers questions about the run's structure in plain
81
+ words, such as *"what links to patients?"*, *"how are payments and tags connected?"* or
82
+ *"where is email?"*. It uses fixed templates, no language model and no data access. Every
83
+ answer says how it read the question and how sure SSDO is. Questions about the data
84
+ itself ("how many patients?") are declined with the reason. It is also available as
85
+ `POST /v1/understandings/{run_id}/ask`; see `docs/specs/tier2-ask.md`. It also answers
86
+ *"what changed since the last run?"* and *"what is a junction table?"*, suggests questions
87
+ suited to the portal mode, and in Study mode defines each technical word it uses.
88
+
89
+ The left panel holds the **mode** (Development opens runs in the technical view, Study in
90
+ Teach me, Enterprise in Explain simply; every view stays reachable from every mode), an
91
+ opt-in **Keep this session** (off by default; remembers the mode and the last 3
92
+ connections on this device, never the password, user or API key), and your **run
93
+ history** with a Compare button for any database profiled more than once.
94
+
95
+ Authenticate with the `X-API-Key` header. With no keys configured the server runs in
96
+ dev mode and accepts `dev-local-key`; for real use set
97
+ `SSDO_API_KEYS="key1:tenant-a,key2:tenant-b"`.
98
+
99
+ SSDO stores its own results in `ssdo_platform.db` (SQLite) by default; set
100
+ `SSDO_DB_URL` to use another database. This file is local and not committed.
101
+
102
+ ## Production settings
103
+
104
+ | Variable | Purpose | Default |
105
+ |---|---|---|
106
+ | `SSDO_ENV` | `production` refuses to start without API keys and tightens the defaults below | `development` |
107
+ | `SSDO_API_KEYS` | `key:tenant` pairs, comma-separated | none (dev key in development) |
108
+ | `SSDO_ALLOWED_DB_HOSTS` | Private/loopback hosts, IPs or CIDRs `/v1/profile` may reach in production, e.g. `db.internal,10.20.0.0/16` | none |
109
+ | `SSDO_CORS_ORIGINS` | Allowed browser origins | `*` in development, none in production |
110
+ | `SSDO_PROFILE_RATE_LIMIT` | Profiling calls per API key, `calls/seconds`; `0` disables | `10/60` |
111
+ | `SSDO_DB_URL` | SSDO's own store | `sqlite:///ssdo_platform.db` |
112
+
113
+ Link-local addresses (including the cloud metadata address 169.254.169.254) are refused
114
+ in every mode. See `ssdo/api/security.py` for the reasoning.
115
+
116
+ ## SSDO's own store and migrations
117
+
118
+ The store's schema is managed by Alembic (`ssdo/db/migrations`). **You normally run
119
+ nothing:** the API and scripts upgrade the store automatically on startup, including
120
+ stores created before migrations existed. Your runs are kept.
121
+
122
+ Everyday commands (safe any time):
123
+
124
+ ```bash
125
+ python -m alembic current # which version the store is at
126
+ python -m alembic upgrade head # upgrade now instead of waiting for startup
127
+ ```
128
+
129
+ **Only when you change a table in `ssdo/db/models.py`**, draft a migration for it:
130
+
131
+ ```bash
132
+ python -m alembic revision --autogenerate -m "add xyz to runs" # then review the file
133
+ ```
134
+
135
+ If no model changed, this writes no file. `tests/db/test_migrations.py` fails if the
136
+ models and migrations ever disagree.
137
+
138
+ ## Encrypted connections (TLS)
139
+
140
+ Every profile request chooses how the connection to the database is protected (console:
141
+ **Encryption (TLS)**; API: `tls` and `tls_ca`; command line: `--tls`, `--tls-ca`). The same
142
+ five modes apply to MySQL and PostgreSQL:
143
+
144
+ | `tls` | Encrypted | Server certificate checked | Use it for |
145
+ |---|---|---|---|
146
+ | `off` | never | — | a local database without TLS |
147
+ | `prefer` *(default)* | if the server offers it | no | everyday use; falls back to plain |
148
+ | `require` | always | no | stops eavesdropping, **not** impersonation |
149
+ | `verify-ca` | always | signed by *your* CA (`tls_ca` required) | provider CA, IP address as host |
150
+ | `verify-full` | always | signed by a trusted CA **and** names the host | hosted databases, production |
151
+
152
+ Hosted databases (Aiven, DigitalOcean, Azure, …) usually require TLS: download the
153
+ provider's **CA certificate** and paste it into the console with `verify-full`. Without a
154
+ certificate, `verify-full` trusts the public certificate authorities. The certificate is
155
+ used for one connection and never stored. Failures say what to change, e.g. *"The
156
+ server's TLS certificate does not name this host."*
157
+
158
+ ## Command line
159
+
160
+ ```bash
161
+ python -m scripts.profile_database --database mydb --user readonly --password ... --save
162
+ python -m scripts.profile_database --host db.example.com --database mydb --user readonly \
163
+ --password ... --tls verify-full --tls-ca ca.pem
164
+ python -m scripts.understanding_history --tenant tenant-mydb
165
+ python -m scripts.compare_runs --tenant tenant-mydb
166
+ ```
167
+
168
+ Reports are written to `reports/` (local only, not committed).
169
+
170
+ ## Deploy on Render
171
+
172
+ `render.yaml` is a Render Blueprint. It creates the web service (API and console) and
173
+ the PostgreSQL database SSDO stores its runs in. A Render service's disk is wiped on
174
+ every deploy, so the store must be Postgres, not SQLite.
175
+
176
+ 1. **Create the services.** In Render: **New → Blueprint**, pick this repository, and
177
+ apply. Render builds with `pip install -r requirements.txt`, creates the database
178
+ and wires `SSDO_DB_URL` to it.
179
+ 2. **Let people in.** On the `solunex-ssdo` service → **Environment**, set
180
+ `SSDO_ADMIN_EMAILS` (and/or `SSDO_API_KEYS`). The service will not start in
181
+ production with neither.
182
+ - **Accounts (recommended):** `SSDO_ADMIN_EMAILS=you@example.com` (comma-separated
183
+ for more admins). An admin signs in → **Invites** → **Generate code**, once per
184
+ person. A code works **once**, expires (1 to 30 days), can be revoked, and SSDO stores
185
+ only its fingerprint. **Every new account gets its own private workspace**: nobody
186
+ sees another person's runs. Only a **team** invite, chosen on purpose by an admin,
187
+ lets someone into the admin's workspace and its runs. The console shows every
188
+ person whether their workspace is private or shared, and with how many people.
189
+ - **Legacy static codes:** `SSDO_INVITE_CODES=code:tenant,...` still works so nobody is
190
+ locked out, but each sign-up now gets a private workspace (the tenant part is
191
+ ignored). Remove it once admins hand out generated codes.
192
+ - **Operator keys (optional):** `SSDO_API_KEYS=key:tenant,...` for servers or scripts
193
+ you manage yourself. Every entry must have a `:tenant` part, or the service
194
+ refuses to start and says which entry is wrong.
195
+ Generate a strong operator key with:
196
+ ```bash
197
+ python -c "import secrets; print(secrets.token_urlsafe(24))"
198
+ ```
199
+ The tenant always comes from the credential, never from the request.
200
+ 3. **Open it.** Open the service's `https://….onrender.com` URL, then **Create account**
201
+ with a generated invite code (or paste an operator key). The store's tables are created
202
+ and upgraded automatically on start.
203
+
204
+ **Which databases can be profiled from Render.** Only databases reachable from the
205
+ internet, such as cloud or hosted databases. A database on someone's laptop
206
+ (`localhost`) or office network cannot be reached from Render, and SSDO refuses
207
+ private addresses in production anyway. If the database has a firewall, allow
208
+ Render's outbound IP addresses (listed on the service's page in Render). Always use a
209
+ **read-only** database user.
210
+
211
+ **Free plan.** The free web service sleeps when idle, so the first request after a
212
+ break takes a while. Render's free PostgreSQL is time-limited. Check Render's current
213
+ terms, and move the database to a paid plan before relying on the history.
214
+
215
+ ## The live workspace: watch your database while you code
216
+
217
+ In Development mode, the **Live** page follows a database while you work on it. Run this
218
+ next to your code (it asks for the database password once and keeps it on your machine):
219
+
220
+ ```bash
221
+ SSDO_API_KEY=<your API key> ssdo watch --engine postgres --database myapp_dev --user dev \
222
+ --server https://solunex-ssdo.onrender.com
223
+ ```
224
+
225
+ Every few seconds it reads the database's **structure** (never a row) and sends it to SSDO
226
+ **only when it changed**. Each change appears on the Live page and in the terminal: a table
227
+ added, a column's type changed, a link removed. Each is marked Fact or Inferred. SSDO never
228
+ connects to your database, which is why this works for `localhost` databases. See
229
+ `docs/specs/live-workspace.md`.
230
+
231
+ **A watch is private to whoever starts it**, even in a team workspace. Teammates see neither
232
+ the watch nor the runs it saves. Runs started in the console stay shared with the workspace.
233
+ **Live rooms** share a watch with chosen teammates (`docs/specs/team-live-workspace.md`):
234
+ a room lives inside one team workspace; its owner adds a teammate, who must accept; only a
235
+ watch's owner shares it in, from the console or with `ssdo watch … --room "Payments sprint"`.
236
+ Members see one merged feed ("seen on aisha@…'s `clinic_dev`"), each watch's runs, and
237
+ "compare with a teammate"; never a watch's host and port. Unsharing, leaving or closing the
238
+ room ends access at once. In the console: Live → **Rooms** (invitations, the room's
239
+ feed, compare); Live → **My watches** to share or stop sharing a watch. API:
240
+ `/v1/live/rooms…` and `POST /v1/watches/{id}/share`.
241
+
242
+ **Apply a change from your own terminal** (`docs/specs/build-together.md` §7.2). On one of your
243
+ own watches, open **Change this database**, write a structure change (ALTER, CREATE, DROP,
244
+ RENAME: never rows), press **Explain first**, then **Approve for my terminal**. Nothing runs
245
+ on the server: start the watcher with `--allow-apply`,
246
+
247
+ ```bash
248
+ SSDO_API_KEY=<your API key> ssdo watch --engine mysql --database shop_dev --user dev \
249
+ --server https://solunex-ssdo.onrender.com --allow-apply
250
+ ```
251
+
252
+ and it shows the approved change in your terminal, checks it again against the structure it
253
+ just read, and runs it with its own connection **only after you type `yes`**. Postgres runs
254
+ the whole change in one transaction; MySQL commits each statement, and says how many ran if
255
+ one fails. The console records what your terminal answered (applied, failed with the error's
256
+ code only, or declined), and the feed marks the change "Applied from a draft". Only a watch's
257
+ owner can approve, an approval waits 24 hours, and without a terminal nothing ever runs.
258
+
259
+ ## Explain a migration: `ssdo snapshot` and `ssdo diff`
260
+
261
+ Before a change ships, see what it does to the database's structure. Take a snapshot,
262
+ apply your migrations with your own tool, take another, and compare:
263
+
264
+ ```bash
265
+ export SSDO_DB_PASSWORD=... # read once, never written to the snapshot
266
+ ssdo snapshot --engine postgres --database myapp_dev --user dev --out before.json --label main
267
+ alembic upgrade head # or: python manage.py migrate, npx prisma migrate deploy, ...
268
+ ssdo snapshot --engine postgres --database myapp_dev --user dev --out after.json --label my-branch
269
+ ssdo diff before.json after.json # --format markdown | json, --out FILE
270
+ ```
271
+
272
+ `ssdo diff` is fully offline (no server, no API key, no network). It lists every change as
273
+ Fact or Inferred, what pointed at whatever was removed or retyped, and **needs-care notes**:
274
+ values that a removed column discards, a narrower type, a NOT NULL that fails on existing
275
+ rows, a link that is no longer declared, a changed key, a removed index. Each note says
276
+ what SSDO cannot know, because it never reads data. SSDO never runs a migration.
277
+
278
+ It never fails by default. A team can choose to: `--fail-on can_lose_values,narrower_type`
279
+ (or `any`) exits 1 when the change has those kinds of notes. The kinds are
280
+ `can_lose_values`, `can_fail_on_rows`, `narrower_type`, `type_conversion`,
281
+ `breaks_declared_link`, `breaks_inferred_link`, `key_changed`, `index_removed`,
282
+ `possible_rename`. In CI, `--format markdown --context ci` gives the pull-request comment.
283
+ See `docs/specs/ci-schema-diff.md`.
284
+
285
+ ### SSDO in CI: a comment on every pull request
286
+
287
+ Add this workflow to your repository. On each pull request it builds a throwaway database
288
+ with **your** migrations, first at the base and then with the pull request applied on top
289
+ (the path production takes), and posts one comment that it updates on every push:
290
+
291
+ ```yaml
292
+ # .github/workflows/schema-diff.yml
293
+ name: schema diff
294
+ on: pull_request
295
+ permissions: { contents: read, pull-requests: write }
296
+ jobs:
297
+ schema:
298
+ runs-on: ubuntu-latest
299
+ services:
300
+ db:
301
+ image: postgres:16
302
+ env: { POSTGRES_PASSWORD: postgres, POSTGRES_DB: app }
303
+ ports: ["5432:5432"]
304
+ options: --health-cmd pg_isready --health-interval 2s --health-retries 30
305
+ steps:
306
+ - uses: saniabduljabbar619-create/solunex-intelligence/.github/actions/schema-diff@v1
307
+ with:
308
+ engine: postgres
309
+ database: app
310
+ user: postgres
311
+ password: postgres # a throwaway database, not a secret
312
+ setup: pip install -r requirements.txt
313
+ migrate: alembic upgrade head
314
+ # fail-on: can_lose_values,narrower_type (optional; off by default)
315
+ ```
316
+
317
+ - **Your migrate command** sees `DATABASE_URL`, `SSDO_DB_HOST`/`_PORT`/`_NAME`/`_USER`/`_PASSWORD`,
318
+ and `SSDO_SIDE` (`base` or `head`). Examples: `python manage.py migrate`,
319
+ `npx prisma migrate deploy`, `flyway migrate`, or `psql "$DATABASE_URL" -f schema.sql`.
320
+ - **MySQL:** use a `mysql:8.4` service (`MYSQL_ROOT_PASSWORD`, `MYSQL_DATABASE`) and `engine: mysql`.
321
+ - **Pull requests from forks** get a read-only token, so GitHub refuses the comment. The
322
+ result is always in the job summary too.
323
+ - **Outputs:** `has-change`, `care-count`, `markdown-file` and `json-file`, for your own steps.
324
+ - **Nothing leaves the job:** SSDO is installed from the action itself and runs offline.
325
+ - **Versions:** `@v1` follows every compatible update of the action. Pin a full commit SHA
326
+ instead if your team prefers to review each update.
327
+
328
+ **Other CI systems** use the two commands directly. A GitLab example:
329
+
330
+ ```yaml
331
+ schema-diff:
332
+ image: python:3.12
333
+ services: [{ name: postgres:16, alias: db }]
334
+ variables: { POSTGRES_PASSWORD: postgres, POSTGRES_DB: app, SSDO_DB_PASSWORD: postgres,
335
+ DATABASE_URL: "postgresql://postgres:postgres@db:5432/app" }
336
+ rules: [{ if: $CI_PIPELINE_SOURCE == "merge_request_event" }]
337
+ script:
338
+ - pip install "git+https://github.com/saniabduljabbar619-create/solunex-intelligence" -r requirements.txt
339
+ - git fetch origin $CI_MERGE_REQUEST_TARGET_BRANCH_NAME
340
+ - git checkout FETCH_HEAD && alembic upgrade head
341
+ - ssdo snapshot --engine postgres --host db --database app --user postgres --out base.json --label base
342
+ - git checkout $CI_COMMIT_SHA && alembic upgrade head
343
+ - ssdo snapshot --engine postgres --host db --database app --user postgres --out head.json --label head
344
+ - ssdo diff base.json head.json --format markdown --context ci | tee schema-diff.md
345
+ artifacts: { paths: [schema-diff.md] }
346
+ ```
347
+
348
+ ## Feedback and the Admin page
349
+
350
+ - **Message the Solunex team** (bottom of the left panel) sends an idea, a problem, a
351
+ question or a pricing question to the **people** at Solunex. It is deliberately styled
352
+ unlike Ask SSDO, and says so: a person reads it. Unlike an Ask question it is stored, so
353
+ it can be answered; it is never written to the server log. Five messages an hour per
354
+ workspace.
355
+ - **Admin** (admins only, `SSDO_ADMIN_EMAILS`): sign-ups and runs per day, totals, live
356
+ watches, the latest sign-ups, and the message inbox (mark read, done, reply by email).
357
+ **Counts only**: admins never see another workspace's databases, tables or runs.
358
+ - **Be told when a message arrives** (optional, set on Render → Environment):
359
+ - Telegram: create a bot with @BotFather, then set `SSDO_NOTIFY_TELEGRAM_TOKEN` (the bot
360
+ token) and `SSDO_NOTIFY_TELEGRAM_CHAT` (your chat id: message the bot once, then open
361
+ `https://api.telegram.org/bot<token>/getUpdates` and copy `chat.id`).
362
+ - Email: `SSDO_NOTIFY_EMAIL_TO`, `SSDO_SMTP_HOST`, `SSDO_SMTP_PORT` (587), `SSDO_SMTP_USER`,
363
+ `SSDO_SMTP_PASSWORD` (for Gmail, an app password), optionally `SSDO_SMTP_FROM`.
364
+ With neither set, messages still arrive on the Admin page.
365
+
366
+ ## Accounts and API keys
367
+
368
+ - **Sign in** in the left panel. The console then authenticates with your session: no
369
+ key to paste. The session lives in this browser tab only, unless **Keep this
370
+ session** is on; it expires after 14 days, and **Sign out** ends it on the server.
371
+ - **API keys** (signed in → *API keys*) are for scripts and tools: send one as the
372
+ `X-API-Key` header. A key is shown **once** when created. SSDO stores only its SHA-256
373
+ fingerprint, so a lost key is revoked and replaced, never recovered. Keys cannot
374
+ create or revoke other keys; only a signed-in person can.
375
+ - **Passwords** are stored only as salted scrypt hashes. Sign-in and sign-up are
376
+ rate-limited, and an unknown email and a wrong password get the same answer.
377
+ - **Sign out** forgets this person in the browser: the remembered connections are cleared
378
+ and the page reloads, so nothing of theirs is left for the next person. Remembered
379
+ connections belong to one account; a different account signing in never sees them.
380
+ - **Local development** with no `SSDO_ADMIN_EMAILS` and no `SSDO_INVITE_CODES`: sign-up
381
+ is open (each account private), and the dev key `dev-local-key` still works.
382
+
383
+ ## Tests
384
+
385
+ ```bash
386
+ python -m pytest
387
+ ```
388
+
389
+ The Postgres profiler also has live tests against a real server (skipped by default).
390
+ They create a throwaway database, profile it as a read-only user, and drop it:
391
+
392
+ ```bash
393
+ SSDO_TEST_PG_ADMIN="host=127.0.0.1 user=postgres password=..." python -m pytest tests/profiler
394
+ ```
395
+
396
+ The TLS modes have live tests too, against TLS-enabled servers whose certificate names
397
+ `localhost` and is signed by the CA at `ca=` (see `tests/profiler/test_tls.py`):
398
+
399
+ ```bash
400
+ SSDO_TEST_MYSQL_TLS="host=localhost port=3306 user=ro password=... database=shop ca=ca.pem" \
401
+ SSDO_TEST_PG_TLS="host=localhost port=5432 user=ro password=... database=postgres ca=ca.pem" \
402
+ SSDO_TEST_MYSQL_NOTLS="host=127.0.0.1 port=3308 user=ro password=... database=shop" \
403
+ python -m pytest tests/profiler/test_tls.py
404
+ ```