erdscope 0.5.0__tar.gz → 0.6.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 (25) hide show
  1. {erdscope-0.5.0/erdscope.egg-info → erdscope-0.6.0}/PKG-INFO +129 -4
  2. {erdscope-0.5.0 → erdscope-0.6.0}/README.md +128 -3
  3. {erdscope-0.5.0 → erdscope-0.6.0}/erd.py +945 -71
  4. {erdscope-0.5.0 → erdscope-0.6.0/erdscope.egg-info}/PKG-INFO +129 -4
  5. {erdscope-0.5.0 → erdscope-0.6.0}/erdscope.egg-info/SOURCES.txt +3 -0
  6. {erdscope-0.5.0 → erdscope-0.6.0}/pyproject.toml +1 -1
  7. {erdscope-0.5.0 → erdscope-0.6.0}/tests/test_config_validation.py +389 -0
  8. {erdscope-0.5.0 → erdscope-0.6.0}/tests/test_e2e.py +521 -0
  9. {erdscope-0.5.0 → erdscope-0.6.0}/tests/test_erd.py +116 -0
  10. erdscope-0.6.0/tests/test_groups.py +325 -0
  11. erdscope-0.6.0/tests/test_notes.py +556 -0
  12. {erdscope-0.5.0 → erdscope-0.6.0}/tests/test_pipeline.py +73 -8
  13. erdscope-0.6.0/tests/test_provider_contract.py +221 -0
  14. {erdscope-0.5.0 → erdscope-0.6.0}/LICENSE +0 -0
  15. {erdscope-0.5.0 → erdscope-0.6.0}/erdscope.egg-info/dependency_links.txt +0 -0
  16. {erdscope-0.5.0 → erdscope-0.6.0}/erdscope.egg-info/entry_points.txt +0 -0
  17. {erdscope-0.5.0 → erdscope-0.6.0}/erdscope.egg-info/requires.txt +0 -0
  18. {erdscope-0.5.0 → erdscope-0.6.0}/erdscope.egg-info/top_level.txt +0 -0
  19. {erdscope-0.5.0 → erdscope-0.6.0}/setup.cfg +0 -0
  20. {erdscope-0.5.0 → erdscope-0.6.0}/tests/test_build.py +0 -0
  21. {erdscope-0.5.0 → erdscope-0.6.0}/tests/test_characterization.py +0 -0
  22. {erdscope-0.5.0 → erdscope-0.6.0}/tests/test_db_integration.py +0 -0
  23. {erdscope-0.5.0 → erdscope-0.6.0}/tests/test_demo.py +0 -0
  24. {erdscope-0.5.0 → erdscope-0.6.0}/tests/test_merge_ir.py +0 -0
  25. {erdscope-0.5.0 → erdscope-0.6.0}/tests/test_rails_schema.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: erdscope
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: Interactive, self-contained ER-diagram HTML and Excel table definitions from a MySQL, PostgreSQL, or SQLite database and/or application code (Rails / Prisma / Django) — single file, zero required dependencies
5
5
  Author: tas6
6
6
  License-Expression: MIT
@@ -37,9 +37,15 @@ Dynamic: license-file
37
37
  [![CI](https://github.com/orapli/erdscope/actions/workflows/ci.yml/badge.svg)](https://github.com/orapli/erdscope/actions/workflows/ci.yml)
38
38
  [![PyPI](https://img.shields.io/pypi/v/erdscope)](https://pypi.org/project/erdscope/)
39
39
 
40
- Generate a **self-contained, interactive ER diagram** and an **Excel table-definition
40
+ **Interactive ER diagrams and documented schema definitions.** Generate a
41
+ **self-contained, interactive ER diagram** — and an **Excel table-definition
41
42
  workbook** — from a live MySQL, PostgreSQL, or SQLite database, with a single-file,
42
- zero-dependency Python CLI.
43
+ zero-dependency Python CLI. Turn an ER diagram into a documented schema: attach design
44
+ decisions, operational rules, and ADR links to tables and relationships with config
45
+ [`notes:`](#notes-attach-design-decisions-to-the-diagram) — plain text and http(s) links
46
+ only, validated against the real schema so a note can never point at something that
47
+ doesn't exist — and draw a rounded, titled frame around a set of related tables with config
48
+ [`groups:`](#groups-draw-a-frame-around-related-tables).
43
49
 
44
50
  ```bash
45
51
  pip install erdscope
@@ -248,6 +254,12 @@ sources:
248
254
  (The optional top-level `version: 1` is a config-format marker — currently the only
249
255
  supported value, with no other effect; it's for future config-format changes to key off.)
250
256
 
257
+ A typed source that parses **nothing** — say, a Prisma project accidentally declared as
258
+ `rails.models` — is a hard error naming the source id and the layout the type expected,
259
+ never a silently empty diagram. If an empty result is genuinely intended (a scaffolded
260
+ but still-empty `app/models`, for example), opt in per source with `allow_empty: true`;
261
+ a `rails.project` entry passes the flag down to both of its expanded halves.
262
+
251
263
  A **`rails.project`** entry is a macro for a whole Rails app root: it expands to both the
252
264
  `rails.schema` (`<root>/db/schema.rb`) and `rails.models` (`<root>/app/models`) halves,
253
265
  whichever exist —
@@ -267,6 +279,91 @@ closer to the real database than code is, but a live DB read still wins when bot
267
279
  present. See the [Input sources chapter of the manual](https://orapli.github.io/erdscope/manual.html#input-sources)
268
280
  for the full `sources[]` reference.
269
281
 
282
+ ### Notes: attach design decisions to the diagram
283
+
284
+ Config **`notes:`** attaches short, plain-text write-ups — design decisions, operational
285
+ rules, ADR links — to a table, a specific relation, or the whole diagram. Notes never
286
+ touch the schema itself (no effect on columns, associations, or merge precedence); they're
287
+ a read-only sidecar validated against the final merged schema and rendered next to it:
288
+
289
+ ```yaml
290
+ notes:
291
+ - id: user-retention
292
+ target: { type: table, table: users }
293
+ title: Retention policy
294
+ text: Suspended accounts are kept for 1 year, then anonymized.
295
+ links:
296
+ - { label: ADR-004, url: https://example.com/adr/004 }
297
+
298
+ - id: order-ownership
299
+ target: { type: relation, source_table: orders, target_table: users, foreign_key: user_id }
300
+ text: Orders are kept after a user is anonymized (financial record-keeping).
301
+
302
+ - id: diagram-conventions
303
+ target: { type: global }
304
+ title: How to read this diagram
305
+ text: The dotted amber edge is an inferred relation, not a real FK.
306
+ ```
307
+
308
+ - Every note needs a config-unique `id` and non-empty `text`; `title` and `links` are
309
+ optional. Link `url`s must be `http://` or `https://` — anything else (`javascript:`,
310
+ `data:`, a bare string) is rejected at load time.
311
+ - `target.type` is `table`, `relation`, or `global`. A `relation` note is identified by
312
+ `source_table` (the side that **holds** the association — the `belongs_to`/FK-holding
313
+ side for a `belongs_to`, the owning side for a `has_many`) and `target_table`;
314
+ `foreign_key`/`name`/`assoc_type`/`through`/`polymorphic` are optional narrowing keys for
315
+ when a table has more than one relation to the same target (`assoc_type` is the
316
+ association kind — `has_many`/`belongs_to`/`has_one`/`has_and_belongs_to_many` — which
317
+ tells apart, say, a `has_many` and a `has_one` that share a name and target). A note that
318
+ matches no relation, or more than one, is a hard error naming the note's `id` — never a
319
+ silent guess.
320
+ - Validation runs **twice**, like `tables:` above: syntax at load time, then semantically
321
+ against the schema that results **after** DB/`--models`/config `tables:` are all merged
322
+ — so a note can reference a table or relation that config itself adds, and a note on
323
+ something config *removes* is correctly an error.
324
+ - Rendered as plain, HTML-escaped text only — no Markdown, no raw HTML, no scripts — in
325
+ the table's detail panel, next to the matching relation, or in the diagram's legend for
326
+ a `global` note. Notes are also searchable (title, text, link labels). A note whose
327
+ table is hidden from the current view simply doesn't appear in the (now absent) detail
328
+ panel; a `global` note's legend entry is unaffected.
329
+ - `write_excel` accepts the same notes but doesn't use them yet in this release — an Excel
330
+ Notes sheet is a possible future addition, not implemented today.
331
+
332
+ See the [Notes chapter of the manual](https://orapli.github.io/erdscope/manual.html#notes)
333
+ for the full reference.
334
+
335
+ ### Groups: draw a frame around related tables
336
+
337
+ Config **`groups:`** draws a rounded frame with a title around a set of related tables in
338
+ the diagram — a lightweight way to call out a domain ("Billing", "Orders") without changing
339
+ the schema or the layout. Like `notes:`, it's a read-only sidecar validated against the
340
+ final merged schema:
341
+
342
+ ```yaml
343
+ groups:
344
+ - id: billing
345
+ title: Billing
346
+ tables: [invoices, payments, coupons]
347
+ color: "#0d9488"
348
+
349
+ - id: catalog
350
+ tables: [products, categories, product_categories]
351
+ ```
352
+
353
+ - Every group needs a config-unique `id` and non-empty `tables`; `title` (defaults to `id`)
354
+ and `color` (a hex string, e.g. `#0d9488`) are optional.
355
+ - A table may belong to **at most one** group — claiming the same table from two groups is
356
+ a hard error naming both group `id`s and the table, not a silently-picked winner. There's
357
+ no support for overlapping/nested groups in this release.
358
+ - Validation runs twice, like `notes:`/`tables:` above: syntax at load time, then
359
+ semantically against the final merged schema, so a group can reference a table config
360
+ itself adds, and a group naming something config *removes* is correctly an error.
361
+ - Purely visual: groups never affect layout, merge precedence, or associations — the frame
362
+ is drawn around wherever its member tables already ended up. In the viewer, drag a
363
+ group's title to move every member together; a "Groups" toolbar toggle shows/hides the
364
+ frames, and both PNG and SVG exports include them. `--only`/`--exclude` narrow a group's
365
+ membership down to the surviving tables, dropping the group entirely if none remain.
366
+
270
367
  ## Dependencies
271
368
 
272
369
  `erd.py` runs with **zero required dependencies** — everything below is optional, and
@@ -292,6 +389,24 @@ Test-only, and only if you run that particular suite:
292
389
  | [openpyxl](https://pypi.org/project/openpyxl/) | Roundtrip-verifying `--excel` output in the unit tests (`tests/test_erd.py`); that one test skips itself if it's missing |
293
390
  | [Playwright](https://playwright.dev/python/) | The browser E2E suite (`tests/test_e2e.py`) — see [Tests](#tests) below |
294
391
 
392
+ ## Verified versions
393
+
394
+ The input formats erdscope parses change rarely, so exact versions matter less than
395
+ they might seem — newer releases are expected to keep working, though constructs
396
+ beyond the tested ones may be ignored rather than parsed. For the record, this is what
397
+ each input is actually verified against:
398
+
399
+ | Input | Verified against |
400
+ |---|---|
401
+ | MySQL | **8.4** — real-server integration tests in CI (`information_schema`); developed against 8.x |
402
+ | PostgreSQL | **16** — real-server integration tests in CI (`pg_catalog`/`information_schema`) |
403
+ | SQLite | the `sqlite3` module bundled with CPython (any supported 3.x) |
404
+ | Rails `schema.rb` | the format Rails **7.x / 8.x** writes (`ActiveRecord::Schema[7.x]`, and the classic un-versioned header) |
405
+ | Rails models | the association DSL as of Rails **7.x** — `has_many`/`has_one`/`belongs_to`/`has_and_belongs_to_many`, `through:`, `polymorphic:`, STI, concerns, custom base classes; also exercised against Mastodon's real codebase. Dynamically computed definitions (and `structure.sql`) are out of scope |
406
+ | Prisma | the schema language as of Prisma **5 / 6** — `@map`/`@@map`, enums, named relations, implicit and explicit m2m, self-relations, composite `@@id`/`@@unique`, `@@schema` |
407
+ | Django | models as of Django **4.2 / 5.x** — FK / OneToOne / M2M (incl. `through=`), abstract bases, `db_table`/`db_column`, `GenericForeignKey` (kept as a polymorphic marker); a swappable `AUTH_USER_MODEL` FK keeps its column and skips the edge |
408
+ | Python | 3.9+ (`requires-python`); CI runs the latest CPython 3.x |
409
+
295
410
  ## What you get
296
411
 
297
412
  Feature highlights — each link goes to the relevant [manual](https://orapli.github.io/erdscope/manual.html) chapter:
@@ -316,6 +431,12 @@ Feature highlights — each link goes to the relevant [manual](https://orapli.gi
316
431
  the live view and exports
317
432
  - **Extras** — a built-in `?` shortcuts/help popup, dark mode, print stylesheet,
318
433
  resizable/collapsible panes
434
+ - **[Notes](#notes-attach-design-decisions-to-the-diagram)** — attach design decisions,
435
+ operational rules, and ADR links to a table, a relation, or the whole diagram, validated
436
+ against the real schema and searchable alongside tables/columns
437
+ - **[Groups](#groups-draw-a-frame-around-related-tables)** — draw a rounded, titled frame
438
+ around a set of related tables to call out a domain, purely visual, draggable by its
439
+ title, with its own toolbar toggle and export support
319
440
 
320
441
  ## Tests
321
442
 
@@ -324,7 +445,11 @@ python3 -m unittest discover -s tests -v
324
445
  ```
325
446
 
326
447
  The IR builders and the Excel writer are covered by pure unit tests; the overlay
327
- parsers have minimal fixtures under `tests/fixture_*`. No database is required to
448
+ parsers have fixtures under `tests/fixture_*` basic plus advanced constructs per
449
+ framework — and every input provider is additionally held to one shared contract
450
+ (`tests/test_provider_contract.py`): typed dispatch, standalone HTML/Excel generation,
451
+ merging over a DB layer, 1:N / 1:1 / M:N / self-reference, association provenance, and
452
+ empty-input diagnostics, all over the same small domain. No database is required to
328
453
  run the tests.
329
454
 
330
455
  `tests/test_e2e.py` drives the generated HTML's client-side JS (grid layout,
@@ -3,9 +3,15 @@
3
3
  [![CI](https://github.com/orapli/erdscope/actions/workflows/ci.yml/badge.svg)](https://github.com/orapli/erdscope/actions/workflows/ci.yml)
4
4
  [![PyPI](https://img.shields.io/pypi/v/erdscope)](https://pypi.org/project/erdscope/)
5
5
 
6
- Generate a **self-contained, interactive ER diagram** and an **Excel table-definition
6
+ **Interactive ER diagrams and documented schema definitions.** Generate a
7
+ **self-contained, interactive ER diagram** — and an **Excel table-definition
7
8
  workbook** — from a live MySQL, PostgreSQL, or SQLite database, with a single-file,
8
- zero-dependency Python CLI.
9
+ zero-dependency Python CLI. Turn an ER diagram into a documented schema: attach design
10
+ decisions, operational rules, and ADR links to tables and relationships with config
11
+ [`notes:`](#notes-attach-design-decisions-to-the-diagram) — plain text and http(s) links
12
+ only, validated against the real schema so a note can never point at something that
13
+ doesn't exist — and draw a rounded, titled frame around a set of related tables with config
14
+ [`groups:`](#groups-draw-a-frame-around-related-tables).
9
15
 
10
16
  ```bash
11
17
  pip install erdscope
@@ -214,6 +220,12 @@ sources:
214
220
  (The optional top-level `version: 1` is a config-format marker — currently the only
215
221
  supported value, with no other effect; it's for future config-format changes to key off.)
216
222
 
223
+ A typed source that parses **nothing** — say, a Prisma project accidentally declared as
224
+ `rails.models` — is a hard error naming the source id and the layout the type expected,
225
+ never a silently empty diagram. If an empty result is genuinely intended (a scaffolded
226
+ but still-empty `app/models`, for example), opt in per source with `allow_empty: true`;
227
+ a `rails.project` entry passes the flag down to both of its expanded halves.
228
+
217
229
  A **`rails.project`** entry is a macro for a whole Rails app root: it expands to both the
218
230
  `rails.schema` (`<root>/db/schema.rb`) and `rails.models` (`<root>/app/models`) halves,
219
231
  whichever exist —
@@ -233,6 +245,91 @@ closer to the real database than code is, but a live DB read still wins when bot
233
245
  present. See the [Input sources chapter of the manual](https://orapli.github.io/erdscope/manual.html#input-sources)
234
246
  for the full `sources[]` reference.
235
247
 
248
+ ### Notes: attach design decisions to the diagram
249
+
250
+ Config **`notes:`** attaches short, plain-text write-ups — design decisions, operational
251
+ rules, ADR links — to a table, a specific relation, or the whole diagram. Notes never
252
+ touch the schema itself (no effect on columns, associations, or merge precedence); they're
253
+ a read-only sidecar validated against the final merged schema and rendered next to it:
254
+
255
+ ```yaml
256
+ notes:
257
+ - id: user-retention
258
+ target: { type: table, table: users }
259
+ title: Retention policy
260
+ text: Suspended accounts are kept for 1 year, then anonymized.
261
+ links:
262
+ - { label: ADR-004, url: https://example.com/adr/004 }
263
+
264
+ - id: order-ownership
265
+ target: { type: relation, source_table: orders, target_table: users, foreign_key: user_id }
266
+ text: Orders are kept after a user is anonymized (financial record-keeping).
267
+
268
+ - id: diagram-conventions
269
+ target: { type: global }
270
+ title: How to read this diagram
271
+ text: The dotted amber edge is an inferred relation, not a real FK.
272
+ ```
273
+
274
+ - Every note needs a config-unique `id` and non-empty `text`; `title` and `links` are
275
+ optional. Link `url`s must be `http://` or `https://` — anything else (`javascript:`,
276
+ `data:`, a bare string) is rejected at load time.
277
+ - `target.type` is `table`, `relation`, or `global`. A `relation` note is identified by
278
+ `source_table` (the side that **holds** the association — the `belongs_to`/FK-holding
279
+ side for a `belongs_to`, the owning side for a `has_many`) and `target_table`;
280
+ `foreign_key`/`name`/`assoc_type`/`through`/`polymorphic` are optional narrowing keys for
281
+ when a table has more than one relation to the same target (`assoc_type` is the
282
+ association kind — `has_many`/`belongs_to`/`has_one`/`has_and_belongs_to_many` — which
283
+ tells apart, say, a `has_many` and a `has_one` that share a name and target). A note that
284
+ matches no relation, or more than one, is a hard error naming the note's `id` — never a
285
+ silent guess.
286
+ - Validation runs **twice**, like `tables:` above: syntax at load time, then semantically
287
+ against the schema that results **after** DB/`--models`/config `tables:` are all merged
288
+ — so a note can reference a table or relation that config itself adds, and a note on
289
+ something config *removes* is correctly an error.
290
+ - Rendered as plain, HTML-escaped text only — no Markdown, no raw HTML, no scripts — in
291
+ the table's detail panel, next to the matching relation, or in the diagram's legend for
292
+ a `global` note. Notes are also searchable (title, text, link labels). A note whose
293
+ table is hidden from the current view simply doesn't appear in the (now absent) detail
294
+ panel; a `global` note's legend entry is unaffected.
295
+ - `write_excel` accepts the same notes but doesn't use them yet in this release — an Excel
296
+ Notes sheet is a possible future addition, not implemented today.
297
+
298
+ See the [Notes chapter of the manual](https://orapli.github.io/erdscope/manual.html#notes)
299
+ for the full reference.
300
+
301
+ ### Groups: draw a frame around related tables
302
+
303
+ Config **`groups:`** draws a rounded frame with a title around a set of related tables in
304
+ the diagram — a lightweight way to call out a domain ("Billing", "Orders") without changing
305
+ the schema or the layout. Like `notes:`, it's a read-only sidecar validated against the
306
+ final merged schema:
307
+
308
+ ```yaml
309
+ groups:
310
+ - id: billing
311
+ title: Billing
312
+ tables: [invoices, payments, coupons]
313
+ color: "#0d9488"
314
+
315
+ - id: catalog
316
+ tables: [products, categories, product_categories]
317
+ ```
318
+
319
+ - Every group needs a config-unique `id` and non-empty `tables`; `title` (defaults to `id`)
320
+ and `color` (a hex string, e.g. `#0d9488`) are optional.
321
+ - A table may belong to **at most one** group — claiming the same table from two groups is
322
+ a hard error naming both group `id`s and the table, not a silently-picked winner. There's
323
+ no support for overlapping/nested groups in this release.
324
+ - Validation runs twice, like `notes:`/`tables:` above: syntax at load time, then
325
+ semantically against the final merged schema, so a group can reference a table config
326
+ itself adds, and a group naming something config *removes* is correctly an error.
327
+ - Purely visual: groups never affect layout, merge precedence, or associations — the frame
328
+ is drawn around wherever its member tables already ended up. In the viewer, drag a
329
+ group's title to move every member together; a "Groups" toolbar toggle shows/hides the
330
+ frames, and both PNG and SVG exports include them. `--only`/`--exclude` narrow a group's
331
+ membership down to the surviving tables, dropping the group entirely if none remain.
332
+
236
333
  ## Dependencies
237
334
 
238
335
  `erd.py` runs with **zero required dependencies** — everything below is optional, and
@@ -258,6 +355,24 @@ Test-only, and only if you run that particular suite:
258
355
  | [openpyxl](https://pypi.org/project/openpyxl/) | Roundtrip-verifying `--excel` output in the unit tests (`tests/test_erd.py`); that one test skips itself if it's missing |
259
356
  | [Playwright](https://playwright.dev/python/) | The browser E2E suite (`tests/test_e2e.py`) — see [Tests](#tests) below |
260
357
 
358
+ ## Verified versions
359
+
360
+ The input formats erdscope parses change rarely, so exact versions matter less than
361
+ they might seem — newer releases are expected to keep working, though constructs
362
+ beyond the tested ones may be ignored rather than parsed. For the record, this is what
363
+ each input is actually verified against:
364
+
365
+ | Input | Verified against |
366
+ |---|---|
367
+ | MySQL | **8.4** — real-server integration tests in CI (`information_schema`); developed against 8.x |
368
+ | PostgreSQL | **16** — real-server integration tests in CI (`pg_catalog`/`information_schema`) |
369
+ | SQLite | the `sqlite3` module bundled with CPython (any supported 3.x) |
370
+ | Rails `schema.rb` | the format Rails **7.x / 8.x** writes (`ActiveRecord::Schema[7.x]`, and the classic un-versioned header) |
371
+ | Rails models | the association DSL as of Rails **7.x** — `has_many`/`has_one`/`belongs_to`/`has_and_belongs_to_many`, `through:`, `polymorphic:`, STI, concerns, custom base classes; also exercised against Mastodon's real codebase. Dynamically computed definitions (and `structure.sql`) are out of scope |
372
+ | Prisma | the schema language as of Prisma **5 / 6** — `@map`/`@@map`, enums, named relations, implicit and explicit m2m, self-relations, composite `@@id`/`@@unique`, `@@schema` |
373
+ | Django | models as of Django **4.2 / 5.x** — FK / OneToOne / M2M (incl. `through=`), abstract bases, `db_table`/`db_column`, `GenericForeignKey` (kept as a polymorphic marker); a swappable `AUTH_USER_MODEL` FK keeps its column and skips the edge |
374
+ | Python | 3.9+ (`requires-python`); CI runs the latest CPython 3.x |
375
+
261
376
  ## What you get
262
377
 
263
378
  Feature highlights — each link goes to the relevant [manual](https://orapli.github.io/erdscope/manual.html) chapter:
@@ -282,6 +397,12 @@ Feature highlights — each link goes to the relevant [manual](https://orapli.gi
282
397
  the live view and exports
283
398
  - **Extras** — a built-in `?` shortcuts/help popup, dark mode, print stylesheet,
284
399
  resizable/collapsible panes
400
+ - **[Notes](#notes-attach-design-decisions-to-the-diagram)** — attach design decisions,
401
+ operational rules, and ADR links to a table, a relation, or the whole diagram, validated
402
+ against the real schema and searchable alongside tables/columns
403
+ - **[Groups](#groups-draw-a-frame-around-related-tables)** — draw a rounded, titled frame
404
+ around a set of related tables to call out a domain, purely visual, draggable by its
405
+ title, with its own toolbar toggle and export support
285
406
 
286
407
  ## Tests
287
408
 
@@ -290,7 +411,11 @@ python3 -m unittest discover -s tests -v
290
411
  ```
291
412
 
292
413
  The IR builders and the Excel writer are covered by pure unit tests; the overlay
293
- parsers have minimal fixtures under `tests/fixture_*`. No database is required to
414
+ parsers have fixtures under `tests/fixture_*` basic plus advanced constructs per
415
+ framework — and every input provider is additionally held to one shared contract
416
+ (`tests/test_provider_contract.py`): typed dispatch, standalone HTML/Excel generation,
417
+ merging over a DB layer, 1:N / 1:1 / M:N / self-reference, association provenance, and
418
+ empty-input diagnostics, all over the same small domain. No database is required to
294
419
  run the tests.
295
420
 
296
421
  `tests/test_e2e.py` drives the generated HTML's client-side JS (grid layout,