coltrace 0.1.0.dev8__tar.gz → 0.1.0.dev10__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 (150) hide show
  1. {coltrace-0.1.0.dev8/src/coltrace.egg-info → coltrace-0.1.0.dev10}/PKG-INFO +5 -2
  2. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/README.md +4 -1
  3. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/backlog.md +14 -1
  4. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/compatibility-checkpoint.md +136 -8
  5. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/decisions.md +18 -0
  6. coltrace-0.1.0.dev10/docs/extraction-architectures.md +66 -0
  7. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/lineage-json.md +6 -0
  8. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/supported-patterns.md +114 -2
  9. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/troubleshooting.md +27 -0
  10. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/databricks_project/README.md +2 -1
  11. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/databricks_project/pipeline_a/main_orchestrator.py +10 -0
  12. coltrace-0.1.0.dev10/examples/databricks_project/pipeline_a/transform/combine_sources.py +9 -0
  13. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/notebook_cell.py +9 -0
  14. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/pyproject.toml +54 -54
  15. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/__init__.py +22 -22
  16. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/analysis.py +1697 -76
  17. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/graph.py +500 -461
  18. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/lineage_models.py +122 -122
  19. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/models.py +92 -92
  20. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/source_readers.py +87 -83
  21. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/sql_sources.py +268 -16
  22. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10/src/coltrace.egg-info}/PKG-INFO +5 -2
  23. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace.egg-info/SOURCES.txt +5 -0
  24. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_databricks_example.py +321 -280
  25. coltrace-0.1.0.dev10/tests/integration/test_extraction_architectures.py +277 -0
  26. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_notebook_reporting.py +11 -0
  27. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_public_project_patterns.py +7 -2
  28. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_resolution_boundaries.py +239 -239
  29. coltrace-0.1.0.dev10/tests/integration/test_result_containers.py +222 -0
  30. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_sql_templates.py +1 -1
  31. coltrace-0.1.0.dev10/tests/integration/test_unified_pipeline_shapes.py +516 -0
  32. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/unit/test_sql_sources.py +509 -419
  33. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/LICENSE +0 -0
  34. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/MANIFEST.in +0 -0
  35. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/accuracy.md +0 -0
  36. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/architecture.md +0 -0
  37. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/column-history.md +0 -0
  38. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/column-journey-checkpoint.md +0 -0
  39. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/database-patterns.md +0 -0
  40. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/database-sources.md +0 -0
  41. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/discovery-json.md +0 -0
  42. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/graph-workspace-checkpoint.md +0 -0
  43. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/history-checkpoint.md +0 -0
  44. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/mvp-checkpoint.md +0 -0
  45. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/nlp-checkpoint.md +0 -0
  46. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/other-column-checkpoint.md +0 -0
  47. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/pandas-sources-proposal.md +0 -0
  48. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/phase-1.md +0 -0
  49. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/read-capture.md +0 -0
  50. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/docs/source-checkpoint.md +0 -0
  51. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/database_pipeline/README.md +0 -0
  52. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/database_pipeline/build_query.py +0 -0
  53. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/database_pipeline/pipeline.py +0 -0
  54. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/database_pipeline/pyproject.toml +0 -0
  55. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/databricks_project/main_orchestrator.py +0 -0
  56. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/databricks_project/pipeline_a/__init__.py +0 -0
  57. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/databricks_project/pipeline_a/extract/__init__.py +0 -0
  58. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/databricks_project/pipeline_a/extract/primary_extract.py +0 -0
  59. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/databricks_project/pipeline_a/extract/quality_extract.py +0 -0
  60. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/databricks_project/pipeline_a/publish/__init__.py +0 -0
  61. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/databricks_project/pipeline_a/publish/serialize_webapp.py +0 -0
  62. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/databricks_project/pipeline_a/transform/__init__.py +0 -0
  63. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/databricks_project/pipeline_a/transform/priority_rank.py +0 -0
  64. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/databricks_project/shared/__init__.py +0 -0
  65. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/databricks_project/shared/config.py +0 -0
  66. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/databricks_project/shared/token_tracker.py +0 -0
  67. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/databricks_project/shared/topic_changes_extract.py +0 -0
  68. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/databricks_project/shared/warehouse.py +0 -0
  69. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/pricing_pipeline/extract/prices.py +0 -0
  70. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/pricing_pipeline/features/pricing.py +0 -0
  71. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/pricing_pipeline/pipeline.py +0 -0
  72. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/pricing_pipeline/transform/pricing.py +0 -0
  73. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/read_capture/README.md +0 -0
  74. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/read_capture/project/input.csv +0 -0
  75. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/read_capture/project/pipeline.py +0 -0
  76. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/read_capture/run_capture.py +0 -0
  77. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/robustness_pipeline/README.md +0 -0
  78. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/robustness_pipeline/extract.py +0 -0
  79. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/robustness_pipeline/pipeline.py +0 -0
  80. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/robustness_pipeline/transform.py +0 -0
  81. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/sample_pipeline/prices.py +0 -0
  82. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/text_pipeline/README.md +0 -0
  83. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/text_pipeline/pipeline.py +0 -0
  84. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/text_pipeline/processing.py +0 -0
  85. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/text_pipeline/readers.py +0 -0
  86. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/examples/text_pipeline/text.py +0 -0
  87. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/setup.cfg +0 -0
  88. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/__main__.py +0 -0
  89. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/callbacks.py +0 -0
  90. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/capture.py +0 -0
  91. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/cli.py +0 -0
  92. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/collector.py +0 -0
  93. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/compatibility.py +0 -0
  94. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/exporters.py +0 -0
  95. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/history.py +0 -0
  96. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/journey.py +0 -0
  97. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/nosql_sources.py +0 -0
  98. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/notebooks.py +0 -0
  99. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/pandas_api.py +0 -0
  100. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/py.typed +0 -0
  101. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/scanner.py +0 -0
  102. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/source_models.py +0 -0
  103. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/templates/graph.html +0 -0
  104. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace/visual_layout.py +0 -0
  105. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace.egg-info/dependency_links.txt +0 -0
  106. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace.egg-info/entry_points.txt +0 -0
  107. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace.egg-info/requires.txt +0 -0
  108. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/src/coltrace.egg-info/top_level.txt +0 -0
  109. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/fixtures/sample_pipeline/extract/prices.py +0 -0
  110. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/fixtures/sample_pipeline/features/pricing.py +0 -0
  111. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/fixtures/sample_pipeline/pipeline.py +0 -0
  112. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/fixtures/sample_pipeline/transform/pricing.py +0 -0
  113. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_annotation_effects.py +0 -0
  114. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_callback_roles.py +0 -0
  115. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_capture_workflow.py +0 -0
  116. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_cli.py +0 -0
  117. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_column_story.py +0 -0
  118. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_compatibility_cli.py +0 -0
  119. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_conditional_flow.py +0 -0
  120. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_database_sources.py +0 -0
  121. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_journey_boundaries.py +0 -0
  122. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_lineage_acceptance.py +0 -0
  123. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_nosql_extraction.py +0 -0
  124. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_other_column_stories.py +0 -0
  125. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_pandas_boundaries.py +0 -0
  126. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_pandas_observation.py +0 -0
  127. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_production_patterns.py +0 -0
  128. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_project_compatibility.py +0 -0
  129. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_reader_inventory.py +0 -0
  130. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_scan_project.py +0 -0
  131. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_source_config.py +0 -0
  132. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_source_precision.py +0 -0
  133. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_sql_reader_boundaries.py +0 -0
  134. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/integration/test_text_pipeline.py +0 -0
  135. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/unit/test_analysis.py +0 -0
  136. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/unit/test_callbacks.py +0 -0
  137. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/unit/test_collector.py +0 -0
  138. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/unit/test_compatibility.py +0 -0
  139. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/unit/test_exporters.py +0 -0
  140. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/unit/test_graph.py +0 -0
  141. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/unit/test_graph_workspace.py +0 -0
  142. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/unit/test_history.py +0 -0
  143. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/unit/test_journey.py +0 -0
  144. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/unit/test_nosql_sources.py +0 -0
  145. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/unit/test_notebooks.py +0 -0
  146. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/unit/test_read_capture.py +0 -0
  147. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/unit/test_scanner.py +0 -0
  148. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/unit/test_source_models.py +0 -0
  149. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/unit/test_source_presentation.py +0 -0
  150. {coltrace-0.1.0.dev8 → coltrace-0.1.0.dev10}/tests/unit/test_visual_layout.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coltrace
3
- Version: 0.1.0.dev8
3
+ Version: 0.1.0.dev10
4
4
  Summary: Static Pandas column lineage, impact analysis, and local dependency graphs
5
5
  Author: Cristiano Diniz
6
6
  License-Expression: MIT
@@ -32,7 +32,7 @@ column versions across direct function calls. Static scanning never imports or
32
32
  executes the project or reads its input data. Optional read capture observes
33
33
  column labels while you explicitly execute your own pipeline.
34
34
 
35
- **Development preview: `0.1.0.dev8`.** The supported subset is deliberately small.
35
+ **Development preview: `0.1.0.dev10`.** The supported subset is deliberately small.
36
36
  Unsupported operations produce diagnostics and may interrupt lineage. A graph
37
37
  with no edge is not proof that a dependency does not exist.
38
38
 
@@ -65,6 +65,9 @@ Query filters are control metadata rather than value dependencies of every
65
65
  output. `spark.sql(build_query(table)).toPandas()` keeps its SQL text even with
66
66
  an injected, unresolved session; SQL assembled from f-strings, `.format`, `%`
67
67
  and joins keeps its known text, with unknown parts shown as `{expression}`.
68
+ `SELECT *`, `UNION`, tables read by name (`spark.table`, Snowpark
69
+ `session.table`, `dbt.ref`) and SQL files read inside the project are modeled
70
+ too; see [extraction architectures](docs/extraction-architectures.md).
68
71
 
69
72
  `report.readers` inventories resolved `pd.read_*` calls with file, line, context,
70
73
  return shape and resulting DataFrame identity. File schemas are unknown unless
@@ -6,7 +6,7 @@ column versions across direct function calls. Static scanning never imports or
6
6
  executes the project or reads its input data. Optional read capture observes
7
7
  column labels while you explicitly execute your own pipeline.
8
8
 
9
- **Development preview: `0.1.0.dev8`.** The supported subset is deliberately small.
9
+ **Development preview: `0.1.0.dev10`.** The supported subset is deliberately small.
10
10
  Unsupported operations produce diagnostics and may interrupt lineage. A graph
11
11
  with no edge is not proof that a dependency does not exist.
12
12
 
@@ -39,6 +39,9 @@ Query filters are control metadata rather than value dependencies of every
39
39
  output. `spark.sql(build_query(table)).toPandas()` keeps its SQL text even with
40
40
  an injected, unresolved session; SQL assembled from f-strings, `.format`, `%`
41
41
  and joins keeps its known text, with unknown parts shown as `{expression}`.
42
+ `SELECT *`, `UNION`, tables read by name (`spark.table`, Snowpark
43
+ `session.table`, `dbt.ref`) and SQL files read inside the project are modeled
44
+ too; see [extraction architectures](docs/extraction-architectures.md).
42
45
 
43
46
  `report.readers` inventories resolved `pd.read_*` calls with file, line, context,
44
47
  return shape and resulting DataFrame identity. File schemas are unknown unless
@@ -71,11 +71,24 @@ not a capability of the published release.
71
71
  | CT-105 | Ranked candidates for ambiguous names | Implemented dev8: `LineageGraph.candidates`, notebook `ON_AMBIGUOUS` |
72
72
  | CT-106 | Cursor `execute` then `fetch*` on separate statements | Proposed: remember the last SQL text executed on a resolved cursor name |
73
73
  | CT-107 | Jinja/dbt templates (`{{ ref() }}`) | Proposed: out of scope until a real project needs it |
74
+ | CT-108 | Dict entries, exact unrolling, accumulators | Implemented dev9 |
75
+ | CT-109 | Filters along the call chain | Implemented dev9 |
76
+ | CT-110 | Open `SELECT *` schemas, UNION, tables by name, SQL files | Implemented dev9 |
77
+ | CT-111 | Extraction census tool | Done in the session scratchpad; promote to `tools/` if it becomes a regular check |
78
+ | CT-112 | `%%bigquery df` cell magic as a SQL source | Proposed (Jupyter/Colab users) |
79
+ | CT-113 | `global`/`nonlocal`, `globals()` reads, per-statement budgets | Implemented dev10 |
80
+ | CT-114 | Row and group iteration, records, scalar value flow | Implemented dev10 |
81
+ | CT-115 | Labels chosen among known strings (read, write, `get`) | Implemented dev10 |
82
+ | CT-116 | Row-wise apply with labeled Series, tuple joins, record exports, introspection builtins | Implemented dev10 |
83
+ | CT-117 | BigQuery `UNNEST`, STRUCT paths, `JOIN USING` | Implemented dev10 |
84
+ | CT-118 | Executor fan-out (`ThreadPoolExecutor.submit`, `as_completed`, `map`) and `tqdm` wrappers | Proposed: seen in LLM enrichment steps of the unified pipeline |
85
+ | CT-119 | Control dependencies of dict values built under guards | Proposed |
86
+ | CT-120 | Series index operations (`s.index[mask]`, `Series.get(label)`) after aggregation | Proposed: seen in a Pareto topic selection |
74
87
  | CT-095 | Lazy materialization | Proposed: avoid eager pass copies of every known column on each row operation for very wide frames |
75
88
 
76
89
  ## Next checkpoint
77
90
 
78
- 1. Rerun the internal compatibility report with dev8: select the target column (now a database column if it is only read inside SQL) and classify the first remaining break.
91
+ 1. Rerun the private pipeline with dev10 and the current notebook cell: select the target database column and confirm that the prioritization outputs and the published exports appear downstream; classify any remaining break.
79
92
  2. Inspect the generated HTML's pan/zoom/selection behavior in a normal browser.
80
93
  3. Run ColTrace against a small real pipeline with a manually checked expected graph.
81
94
  4. Classify missing/incorrect relations by semantic rule, using [accuracy gates](accuracy.md).
@@ -1,7 +1,7 @@
1
1
  # Project compatibility checkpoint
2
2
 
3
- Local checkpoint: `0.1.0.dev8`, 2026-11-04 (dev7 sections below are kept as
4
- history). Publication is a separate step.
3
+ Local checkpoint: `0.1.0.dev10`, 2026-11-05 (dev7 to dev9 sections below are
4
+ kept as history). Publication is a separate step.
5
5
  This work responds to an anonymized compatibility report produced inside the
6
6
  user's company. The private project was not accessed, copied or executed; every
7
7
  fixture below is a new synthetic reproduction of a reported technical shape.
@@ -90,8 +90,10 @@ input, not measured accuracy on the private project.
90
90
 
91
91
  ## Validation
92
92
 
93
- dev8: 984 tests pass (941 in dev7, plus the SQL boundary tests below; SQL
94
- control tests now require database-table columns consumed as row controls).
93
+ dev9: 1,024 tests pass (984 in dev8; dev9 adds container, accumulator, filter
94
+ and extraction-shape tests). dev8: 984 (941 in dev7, plus the SQL boundary
95
+ tests; SQL control tests require database-table columns consumed as row
96
+ controls).
95
97
  Ruff lint/format and strict mypy pass. Lineage snapshots are schema `0.6`
96
98
  (adds `dataframes[].kind`); `0.4` and `0.5` snapshots load as DataFrame frames.
97
99
 
@@ -207,6 +209,129 @@ gaps after it are now reported (+74 `CT_OPAQUE_CALL`); disabling the new text
207
209
  rules reproduces the dev7 numbers exactly. owid analysis time is unchanged
208
210
  (about 39 s). Hand-written precision checks: 14/14 expected, 0/9 forbidden.
209
211
 
212
+ ## Fourth round: the dev8 run and an extraction census (dev9)
213
+
214
+ The user ran dev8 on the private project. Extraction now worked (3,140 column
215
+ versions, 0 errors; physical table fields appeared among modeled names), but
216
+ the run used a pre-dev7 copy of the notebook cell, which printed every
217
+ informational note; the platform truncated the output, and the cell stopped at
218
+ "Column not found with these filters". The visible diagnostics after extraction
219
+ pointed at results kept in dicts and combined with `acc = None` accumulators
220
+ (`tot.merge(...)` on an unresolved receiver, `CT_UNRESOLVED_RECEIVER` runs,
221
+ `pd.concat` and `groupby` on lost frames).
222
+
223
+ Synthetic reproductions confirmed three defects: a dict filled by subscript was
224
+ still the empty literal, so loops over it were skipped silently; iterating
225
+ `frames.items()` invalidated the frames; and a loop inspected once never saw the
226
+ accumulator's `merge` branch. A fourth issue was selection: a database column
227
+ belonged only to the function of its first reading site (often the injected
228
+ `run_query` closure), so a `function=` filter on the extractor excluded it.
229
+ All four are fixed; the synthetic example now includes a dict of results
230
+ combined by an accumulator (`combine_sources.py`).
231
+
232
+ The user also asked for a map of extraction architectures. A static census of
233
+ public projects classified every extraction call shape and compared it with
234
+ ColTrace's output; the result is [extraction architectures](extraction-architectures.md).
235
+ Shapes fixed in this round: `SELECT *` open schemas, `UNION`, tables read by
236
+ name, `.result()` and row-only steps before conversion, literal projections,
237
+ keyword options on conversions, SQL files, and SQL clients whose name looked
238
+ like a DataFrame.
239
+
240
+ Measured on the census projects, extraction sites with SQL fields went from 43
241
+ to 79 and inferred frames of unresolved origin from 68 to 35; conversions whose
242
+ result is never used as a DataFrame stay unmodeled by design (ADR-057, ADR-070).
243
+ On the earlier public harness nothing crashed, every snapshot round-trips, and
244
+ the hand-written precision checks stay at 14/14 expected and 0/9 forbidden.
245
+ From dev9 identical diagnostics are reported once, so warning counts are not
246
+ comparable with earlier rounds (owid: 858 distinct warnings; recommenders: 538).
247
+ owid models more code (16,317 frames and 67,450 column versions, from 15,416
248
+ and 66,426) because paths built from `__file__`, `os.path.join` and known
249
+ `**kwargs` now resolve and known-length loops are unrolled.
250
+
251
+ ## Fifth round: the anonymized unified pipeline (dev10)
252
+
253
+ The user shared an anonymized copy of the whole pipeline with the output of the
254
+ last run, because the target column still did not flow end to end. It is a
255
+ notebook orchestrator that runs two KPI pipelines with `%run`; each extracts
256
+ from BigQuery through injected `run_query` closures, builds weekly facts, trends
257
+ (rank-based trend tests and a change-point library) and a prioritization score,
258
+ then publishes JSON-in-script files and an LLM-assisted reading. The copy was
259
+ analyzed statically outside the repository; nothing from it is part of the
260
+ repository, and the fixtures below are synthetic.
261
+
262
+ Measured with the same script on that copy:
263
+
264
+ | | dev9 (published wheel) | dev10 |
265
+ | --- | --- | --- |
266
+ | Frames / column versions | 404 / 3,189 | 599 / 6,419 |
267
+ | Extraction sources / table frames | 24 / 7 | 40 / 12 |
268
+ | Target database column | not found | 19 downstream columns, through the score to the published export |
269
+ | A database field of the second KPI | not found | 45 downstream columns |
270
+ | `CT_ANALYSIS_LIMIT` | 30 | 0 |
271
+ | `CT_UNRESOLVED_RECEIVER` | 42 | 8 |
272
+ | `CT_UNMODELED_RESULT` | 20 | 1 |
273
+ | `CT_OPAQUE_CALL` | 10 | 6 |
274
+ | `CT_CONDITIONAL_RETURN` | 9 | 2 |
275
+ | `CT_UNRESOLVED_CALL` | 7 | 1 |
276
+ | `CT_DYNAMIC_COLUMN` | 2 | 1 |
277
+ | `CT_PARTIAL_SQL` | 10 | 15 (more SQL is reached; 39 before `UNNEST`/`USING` support) |
278
+
279
+ Causes, in pipeline order (each is a test in
280
+ `tests/integration/test_unified_pipeline_shapes.py`):
281
+
282
+ 1. The first KPI's extract cached its client in a module global
283
+ (`global _client`). Functions declaring `global` were rejected, so the
284
+ warehouse source was never created and the target column did not exist.
285
+ 2. Early notebook cells spent the conditional-path budget
286
+ (30 `CT_ANALYSIS_LIMIT`), which reset bindings in later cells.
287
+ 3. The orchestrator read `%run` results through `globals().get(...)`.
288
+ 4. Trends iterate groups and rows, collect dicts, build frames from records,
289
+ and compute scalars with `list(...values)`, `float`, `round`, `max`, `zip`
290
+ and helpers that return labels under guards.
291
+ 5. The main break: a mask helper picks the period column by a parameter
292
+ (`"month" if kind == ... else "quarter"`). The dynamic read invalidated the
293
+ facts frame just before the ranking, so every ranked value had an opaque
294
+ origin.
295
+ 6. The score is computed by a row-wise `apply` returning `pd.Series({...})`,
296
+ attached with `pd.concat(axis=1)`.
297
+ 7. The ranking returns `(dict, flag)` on several paths, builds the dict with a
298
+ comprehension over `to_dict(orient="records")`, and the caller reads it with
299
+ `.get`.
300
+ 8. `getattr(frame, "columns", [])` and `isinstance(frame, ...)` were opaque
301
+ calls that invalidated the trend frames.
302
+ 9. The second KPI passed `run_query(sql)` straight to a post-processing
303
+ function, so its SQL was never attached to the frame.
304
+ 10. `np.asarray(values)` passed to a change-point library invalidated the trend
305
+ series, and a guarded `return {"flag": False}` was turned into a scalar.
306
+ 11. The second KPI's SQL uses `FROM t AS s, UNNEST(s.items) AS c`, STRUCT fields
307
+ and `LEFT JOIN ... USING (key)`.
308
+
309
+ What remains in that copy: LLM judge steps (thread-pool fan-out, `tqdm`, the LLM
310
+ calls themselves), a topic selection that indexes an aggregated Series by its
311
+ index, and SQL names that anonymization collapsed into duplicates.
312
+
313
+ ### Validation
314
+
315
+ 1,044 tests pass (15 new integration tests in `test_unified_pipeline_shapes.py`
316
+ and 5 new SQL unit tests); Ruff and strict mypy pass. On the public harness
317
+ (13 projects, round 14) nothing crashes, every snapshot round-trips, and the
318
+ hand-written precision checks stay at 14/14 expected and 0/9 forbidden.
319
+ Distinct warnings went from 1,859 to 1,849 in total while more code is modeled
320
+ (owid: 67,450 to 83,918 column versions and 858 to 909 warnings; recommenders:
321
+ 3,495 to 4,179 column versions and 538 to 496 warnings). Analysis time is
322
+ unchanged when projects run alone (owid 54 s). Under memory pressure, owid's
323
+ 572 MB snapshot round-trip in the harness took hours; it is a harness cost, not
324
+ an analysis one.
325
+
326
+ The harness caught one regression before release: scalars computed from cells
327
+ made a library call that also received Series look like a scalar function,
328
+ which hid an opaque call and the frame built from its result. It is fixed and
329
+ covered by `test_unknown_call_receiving_column_views_stays_opaque`. On the
330
+ extraction census, warnings fell in 9 of 18 projects (for example 67 to 39 and
331
+ 41 to 15) and rose by one in two, where newly modeled frames reach unknown
332
+ library calls; one of those calls was `load_table_from_dataframe`, which is now
333
+ a recorded export.
334
+
210
335
  ## Remaining boundaries
211
336
 
212
337
  This is still bounded abstract interpretation, not a general control-flow solver.
@@ -215,10 +340,13 @@ with zero iterations. `groupby(...).apply`, `stack`/`unstack`, `crosstab` and
215
340
  row-wise callbacks with side effects remain unmodeled results. Calls to unknown
216
341
  helpers that receive a DataFrame still interrupt its provenance unless declared
217
342
  as `observers`. Merge suffixes and
218
- row multiplicity are not resolved. Async functions, reflection, `global`/`nonlocal`
219
- writes and properties remain unresolved. `.ipynb` JSON files, live cells and
343
+ row multiplicity are not resolved. Async functions, reflection and properties
344
+ remain unresolved (`global` and `nonlocal` are analyzed from dev10). Executor
345
+ fan-out (`ThreadPoolExecutor.submit`, `as_completed`) and `tqdm` wrappers are
346
+ opaque. `.ipynb` JSON files, live cells and
220
347
  `dbutils.notebook.run` (a separate namespace) are not inputs. Opaque calls that
221
348
  receive a DataFrame still interrupt its provenance.
222
349
 
223
- The next acceptance input is a rerun of the internal compatibility report on
224
- dev7, identifying the first remaining identity break for the target column.
350
+ The next acceptance input is a rerun of the private pipeline with dev10 and
351
+ the current notebook cell, selecting the target database column and confirming
352
+ the prioritization outputs and published exports downstream.
@@ -69,6 +69,24 @@ choices made within the user's authorization.
69
69
  | ADR-062 | Labeled placeholders and last-resort fragment removal | Unknown interpolations are shown as their Python expression; unparsable dynamic fragments are dropped only after every dialect fails, and the schema becomes incomplete |
70
70
  | ADR-063 | Documented conversions keep their receiver's SQL | `spark.sql(text).toPandas()` describes the SELECT rows; a transformation in between drops the query |
71
71
  | ADR-064 | Ranked, never merged, ambiguous candidates | Called code before templates and the earliest origin first; a database column and its same-name SQL output form one selectable family |
72
+ | ADR-065 | Track dict entries; unroll known-length collections | Results kept in dicts were lost silently; exact iteration keeps each frame's identity |
73
+ | ADR-066 | Inspect a second loop iteration for `None` accumulators | `acc = None` patterns need the state one iteration produces; bounded to loops that carry a `None` |
74
+ | ADR-067 | Filters match database columns along the call chain | A table column is used by every extractor that reads it, not only by the closure where SQL runs |
75
+ | ADR-068 | `SELECT *` is an open table schema | Used columns become table columns; lookups through merges of different frames never add columns to a table |
76
+ | ADR-069 | Read SQL files as data | Only inside the analyzed root, SQL/text suffixes, bounded size; never executed |
77
+ | ADR-070 | Keep ADR-057 for conversions | A census suggested promoting every `toPandas()` after SQL; precision tests and ADR-057 keep usage, annotations or contracts as the evidence |
78
+ | ADR-071 | One record per distinct diagnostic | Unrolled iterations and repeated calling contexts produced identical records (the private run showed one line repeated dozens of times); identical records carry no extra information |
79
+ | ADR-072 | Analyze `global` and `nonlocal` | A cached module-level warehouse client was lost because functions declaring `global` were rejected; declared names read the module namespace, resolved calls write back, templates never do |
80
+ | ADR-073 | Budgets per top-level statement | Long notebooks spent the path budget in early cells and cut later ones; each top-level module statement gets the full budget |
81
+ | ADR-074 | Values computed from cells are derived scalars | Builtins, NumPy, `math` and subscripts over cells carry dependencies and never alias a frame; guarded scalar returns depend on the guard's columns, containers keep their structure |
82
+ | ADR-075 | Records are frames; record exports yield records | List-of-dict records build open-schema frames; `to_dict(orient="records")` stays an export and also yields row dicts whose values are cells |
83
+ | ADR-076 | Labels chosen among known strings | A column picked by a parameter invalidated the whole frame; reading every possible column (inferred) is sound and bounded to 8 strings; writes behave like branch joins |
84
+ | ADR-077 | Row-wise apply with labeled Series builds columns | Pandas returns a DataFrame whose columns are the labels; each column reads the row fields of its value |
85
+ | ADR-078 | Join fixed-width tuples by position | `return {}, False` and `return result, ok` lost both positions when merged as unordered members |
86
+ | ADR-079 | Introspection builtins are pure | `isinstance`, `hasattr`, `callable` and literal `getattr` were opaque calls that invalidated frames |
87
+ | ADR-080 | Promote unresolved results at DataFrame parameters | Same evidence as ADR-057, observed in the callee; one frame per result object, so later uses share its identity |
88
+ | ADR-081 | BigQuery `UNNEST`, STRUCT paths and `USING` | Nested fields are columns named by their path; `USING` keys are controls of both sides and an unqualified key is their `COALESCE`; physical tables are assumed to hold a key only as the single remaining candidate |
89
+ | ADR-082 | Warehouse writers are exports | `load_table_from_dataframe`, `write_pandas` and `to_gbq` read the frame they receive; treating them as opaque invalidated it |
72
90
 
73
91
  The initial codename was FeatureTrace. The early architecture proposed reusable
74
92
  function summaries; direct contextual interpretation is used first because it
@@ -0,0 +1,66 @@
1
+ # Extraction architectures
2
+
3
+ How data enters a Pandas pipeline varies more than any other part of it. This
4
+ page maps the extraction shapes ColTrace has met in a real Databricks project
5
+ and in a static census of public projects, and states what the dev9 analysis
6
+ does with each one. Nothing is executed: SQL text, SQL files and notebook
7
+ source are read as data.
8
+
9
+ The census (November 2026) covered Airflow/Astro SDK DAGs, Prefect recipes,
10
+ Dagster and Hamilton examples, the BigQuery Python samples, Snowpark demos, dbt
11
+ Python models, Databricks talks, Kaggle SQL course notebooks, Feast examples, a
12
+ Google Cloud data-engineering course and a small personal ETL repository.
13
+
14
+ ## What a recognized extraction produces
15
+
16
+ - A **source** record with the SQL text (unknown interpolations shown as
17
+ `{expression}`), relations, output fields and row controls.
18
+ - A **DataFrame** whose columns are written by the source.
19
+ - A **database table** frame for every physical relation the SQL reads, with one
20
+ column node per physical column. Selecting such a column (for example a field
21
+ used only inside `SUM(...)` or a `WHERE`) traces it into the DataFrame.
22
+
23
+ ## Map
24
+
25
+ | Shape | Example | dev9 treatment |
26
+ | --- | --- | --- |
27
+ | Pandas SQL readers | `pd.read_sql(sql, con)`, `read_sql_query`, `read_gbq` | Source with fields; dialect from reader or fallback |
28
+ | Connector chains | `bigquery.Client().query(sql).to_dataframe()`, `snowflake ... cursor().execute(sql).fetch_pandas_all()`, `duckdb.sql(sql).df()` | Source with fields when the connector object is resolved |
29
+ | Injected callables | `run_query(sql)` closure or parameter | Followed through closures; otherwise inferred from DataFrame use, SQL fields seeded |
30
+ | Unresolved session or client | `spark.sql(sql).toPandas()`, `client.query(sql).result().to_dataframe(...)`, `conn.execute(sql).fetchdf()` | SQL kept through `.result()` and the conversion; a frame once used as one, returned under a `pd.DataFrame` annotation, or declared by a contract |
31
+ | Tables read by name | `spark.table("db.t")`, `spark.read.table(...)`, Snowpark `session.table("T")`, dbt `dbt.ref("m")`/`dbt.source("s", "t")`, BigQuery `client.list_rows(t)` | Open schema of that table (as `SELECT *`) |
32
+ | Row-only steps before conversion | `.filter(...)`, `.where`, `.limit(n)`, `.distinct()`, `.orderBy`, `.sample` | SQL or table kept |
33
+ | Literal projections before conversion | `.select("A", "B")`, `.drop("SECRET")` | Field list narrowed; a literal `select` makes the schema complete |
34
+ | Spark column expressions | `.withColumn(...)`, `.groupBy(...).agg(...)`, `select(F.col(...))` | SQL dropped: the SELECT list no longer describes the result |
35
+ | `SELECT *` / `t.*` / `* EXCEPT (...)` over one table | `pd.read_sql("SELECT * FROM t", con)` | Open schema: columns the code uses become columns of `t`; merges with other frames never add columns to `t` |
36
+ | `UNION [ALL]` | `SELECT a FROM t UNION SELECT b FROM u` | Labels from the first branch, inputs from every branch by position |
37
+ | f-strings, `.format`, `%`, `+`, `join` | `f"... FROM {TABLE} WHERE d >= '{start}'"` | Known constants, config values and parameter defaults folded; unknown parts shown as `{expression}` |
38
+ | Dynamic fragments | `f"... WHERE x = 1 {extra}"` | Unplaceable fragment ignored as a last resort, with a note |
39
+ | Vendor dialects | BigQuery backticks, Databricks, Snowflake | Reader or call-shape dialect, `[tool.coltrace].sql_dialect`, then fallbacks, with a note |
40
+ | SQL files | `open("sql/q.sql").read()`, `(Path(__file__).parent / "sql" / "q.sql").read_text().format(**params)` | Read as data when inside the analyzed root, SQL/text suffix, at most 200 KB |
41
+ | DB-API parameters | `%s`, `%(name)s`, `?`, `:name`, `@name` | Read as parameters, never as columns |
42
+ | Results kept in containers | `results = {}`; `results["a"] = df`; `for k, d in results.items()` | Dict entries tracked; loops over known-length collections unrolled exactly |
43
+ | Accumulators | `acc = None`; `acc = d if acc is None else acc.merge(d)` | A second iteration is inspected; `None` dropped where the name is used as a frame |
44
+
45
+ ## Boundaries
46
+
47
+ | Shape | Why | What to do |
48
+ | --- | --- | --- |
49
+ | Jinja/dbt SQL templates (`{{ ref() }}`, `{% if %}`) | Template languages are not SQL | Keep a rendered example, or declare a source contract/schema |
50
+ | `cursor.execute(sql)` and `cursor.fetchall()` in separate statements | The query is not attached to the later fetch | Use `pd.read_sql`, or a contract |
51
+ | `%%bigquery df` / `%sql` cell magics | Neutralized as notebook magics | Use the client API in Python cells |
52
+ | Feature stores (`store.get_historical_features(...).to_df()`) | Feature references are not SQL | Declare a source schema |
53
+ | Unknown SDK returning Pandas | No SQL text or table name | `[[tool.coltrace.sources]]` contract with `pandas_method` if needed |
54
+ | Ibis, Polars, Dask, bigframes transformations | Different libraries | Lineage starts at the Pandas conversion |
55
+
56
+ Spelling alone never creates a DataFrame (ADR-057): `x.toPandas()` on an
57
+ unknown `x`, or a conversion whose result is never used as a frame, stays
58
+ unresolved. A source contract makes any of these explicit:
59
+
60
+ ```toml
61
+ [[tool.coltrace.sources]]
62
+ callable = "spark.sql"
63
+ kind = "sql"
64
+ dialect = "databricks"
65
+ pandas_method = "toPandas"
66
+ ```
@@ -243,6 +243,12 @@ fully named relation is one identity across the whole snapshot.
243
243
  `load_report` upgrades 0.4 and 0.5 snapshots by adding `kind: "dataframe"` to
244
244
  their frames; 0.2/0.3 migrations are unchanged.
245
245
 
246
+ From dev10 (still schema 0.6), a nested database field is a `table` column
247
+ named by its path: `items.score` for `UNNEST(s.items) AS c` with `c.score`, and
248
+ `info.flag` for the STRUCT access `s.info.flag`. The diagnostic code
249
+ `CT_LABEL_CHOICE` (info) marks a column read or written through a label chosen
250
+ among known strings.
251
+
246
252
  ## Changes in 0.5
247
253
 
248
254
  Schema 0.5 adds the diagnostic severity `"info"` for recorded inferences and
@@ -419,6 +419,117 @@ raises `AmbiguousColumnError` for distinct lineages (its `candidates` use the
419
419
  same order); ranking never merges them. The notebook cell continues with the
420
420
  first candidate by default (`ON_AMBIGUOUS = "first"`).
421
421
 
422
+ ## Values, labels and records across a notebook pipeline (dev10)
423
+
424
+ The dev9 run on an anonymized two-KPI notebook pipeline lost the target column
425
+ between the warehouse extract and the prioritization score. Each cause below has
426
+ a synthetic reproduction in `tests/integration/test_unified_pipeline_shapes.py`.
427
+
428
+ - **Cached clients and globals.** Functions that declare `global` or `nonlocal`
429
+ are analyzed: a module-level singleton (`_client = None` filled on first use)
430
+ keeps its connector identity, and resolved calls write declared globals back
431
+ to the module (symbolic templates never do). `globals().get("NAME")` and
432
+ `globals()["NAME"]` read the namespace, as notebooks do after `%run`.
433
+ - **Rows and groups.** `for _, row in df.iterrows()`, `itertuples()` and
434
+ `for key, group in df.groupby(...)` bind row cells and group frames;
435
+ `row["c"]`, `row.c` and `row.get("c")` read the column.
436
+ - **Records.** `pd.DataFrame(list_of_dicts)` with known string keys builds a
437
+ frame whose columns are the possible values of each key (inferred, open
438
+ schema). `df.to_dict(orient="records")` is still recorded as an export and
439
+ now also yields row dicts whose values are cells of each column, so
440
+ `{key: row for row in records}` followed by `rows.get(key)["c"]` keeps
441
+ lineage. A dict comprehension is a tracked dict: one inspected entry stands
442
+ for every entry, and its unknown key may match any lookup.
443
+ - **Scalar value flow.** `float`, `int`, `round`, `sum`, `min`, `max`, `abs`,
444
+ `list`, `tuple`, `sorted`, `set`, `.values`, `.tolist()`, `.item()`,
445
+ subscripts and `math.*` carry the dependencies of the cells they read
446
+ (inferred). NumPy and scalar Pandas functions over cells or lists of cells
447
+ (`np.asarray(values)`, `np.mean(x[:i])`, `pd.isna(v)`) return derived values,
448
+ not views of a frame, so passing them to an unresolved library no longer
449
+ invalidates the source frame. A scalar returned under `if slope < 0:` depends
450
+ on the guard's columns (control dependency, inferred); constant containers
451
+ returned under a guard keep their structure.
452
+ - **Pairs and shared dicts.** `zip` and `enumerate` pair known members (an
453
+ unknown list pairs with unknown values); `record["q"] = q` on a dict collected
454
+ by `append` updates every alias of that dict.
455
+ - **Labels chosen at run time.** A value that is one of a few known strings
456
+ (`"a" if flag else "b"`, `if/else` assignments, `LABELS[kind]` over a constant
457
+ mapping or list; at most 8 strings) is a label choice. `df[col]`,
458
+ `df.loc[rows, col]` and `df.get(col)` read each possible column (inferred,
459
+ `CT_LABEL_CHOICE`); a label absent from a closed schema cannot be the chosen
460
+ one. `df[col] = v` gives each candidate column an inferred version that reads
461
+ its previous value and `v`, like a branch join. Before dev10 such a read
462
+ invalidated the whole frame (`CT_DYNAMIC_COLUMN`).
463
+ - **Constant Series.** `pd.Series([False] * len(df), index=df.index)` is a
464
+ Series without column inputs, aligned with `df`; joined with a mask (the
465
+ fallback of a mask helper), it is used as a mask.
466
+ - **Row-wise apply with labeled values.** `df.apply(f, axis=1)` where `f`
467
+ returns `pd.Series({"a": x, "b": y})` builds a frame with one column per
468
+ label, each derived from the row fields its value reads;
469
+ `pd.concat([df, that], axis=1)` attaches the columns.
470
+ - **Tuple returns.** Fixed-width tuples returned on alternative paths
471
+ (`return {}, False` and `return result, ok`) join position by position.
472
+ - **Introspection.** `isinstance(df, ...)`, `hasattr(df, "x")`, `callable(f)`
473
+ and `getattr(obj, "name"[, default])` with a literal name neither read nor
474
+ change column data. `getattr(df, "columns", [])` is `df.columns`; module,
475
+ class and instance attributes resolve as attribute reads.
476
+ - **Unresolved results as arguments.** An unresolved conversion result
477
+ (`client.query(sql).to_dataframe()` on an unknown client) passed straight to a
478
+ parameter used as a DataFrame (`postprocess(run_query(sql))`) is promoted with
479
+ its SQL there (ADR-057 evidence: the callee's DataFrame use), once per result.
480
+ - **Warehouse writers.** `client.load_table_from_dataframe(df, table)`,
481
+ `write_pandas(conn, df, table)` and `pandas_gbq.to_gbq(df, table)` are
482
+ recorded exports, like `spark.createDataFrame(df)`.
483
+ - **Budgets.** The conditional-path and call budgets apply per top-level module
484
+ statement, so a long notebook is not cut by its earlier cells.
485
+ - **BigQuery SQL.** In `FROM t AS s, UNNEST(s.items) AS c`, `c.score` is the
486
+ nested column `items.score` of `t` and `c` alone is the array column;
487
+ `UNNEST(arr) AS u(x)` makes `x` the element. `s.info.flag` is the nested
488
+ column `info.flag`. In `JOIN x USING (k)`, `k` is a control of both sides: a
489
+ CTE or derived table holds `k` when it outputs it, and a physical table when it
490
+ is the only remaining candidate; an unqualified `k` reads both sides
491
+ (`COALESCE`). Before dev10 every `USING` made the summary partial.
492
+
493
+ Still outside after dev10: executor fan-out (`ThreadPoolExecutor.submit`,
494
+ `as_completed`, `executor.map`) and `tqdm` wrappers are opaque; control
495
+ dependencies of dict values built under guards (`{"flag": True}` versus
496
+ `{"flag": False}`) are not recorded; LLM calls are opaque by nature.
497
+
498
+ ## Containers, accumulators and extraction shapes (dev9)
499
+
500
+ The dev8 run on the private project showed lineage lost right after extraction,
501
+ where results are kept in dicts and combined. A census of public projects then
502
+ mapped extraction shapes; see [extraction architectures](extraction-architectures.md).
503
+
504
+ - **Dicts of frames** are tracked entry by entry: literals, `d[k] = df`,
505
+ `d[k]`, `d.get(k)`, `items()`, `values()`, `keys()`, `update`, `setdefault`,
506
+ `pop`, and `f(**known_dict)`. A key that is not statically known may select
507
+ any entry (inferred). Before dev9 a dict filled by subscript stayed the empty
508
+ literal, so loops over it were skipped without a diagnostic.
509
+ - **Loops** over a list, tuple or dict of known length (up to 64 items) are
510
+ unrolled exactly, binding each frame in turn. A literal empty iterable is
511
+ skipped; a name bound to an empty list or dict is still inspected, because it
512
+ may have been filled through an untracked path.
513
+ - **Accumulators**: when a loop assigns a name that is `None` before it, a
514
+ second iteration is inspected so `acc.merge(part)` is seen; a join of `None`
515
+ and frames is the frame wherever the name is used as one.
516
+ - **Selection filters**: `function=`/`module=` match a database column from any
517
+ function that reads it, including the callers on the analysis call chain (an
518
+ extractor that calls an injected `run_query`).
519
+ - **`SELECT *`** over one table is an open schema of that table; **UNION**
520
+ branches are combined by position; **tables read by name**
521
+ (`spark.table`, `session.table`, `dbt.ref`, `dbt.source`, `list_rows`) are
522
+ `SELECT *` of that table; `.result()` and row-only Spark/Snowpark steps keep the
523
+ SQL; literal `select`/`drop` narrow it; conversions accept keyword options.
524
+ - **SQL files** inside the analyzed root are read as text (`open(...).read()`,
525
+ `Path.read_text()`, `Path(__file__).parent / "sql"`, `os.path.join`), never
526
+ executed, and reported with `CT_SQL_FILE`. `__file__` is the module path
527
+ relative to the analyzed root, so snapshots never record local paths.
528
+ - Identical diagnostics (same code, message, location and scope) are reported
529
+ once.
530
+ - `client.query(SQL_CONSTANT)` on a name that looked like a DataFrame is a SQL
531
+ client call: `DataFrame.query` never takes SQL text.
532
+
422
533
  ## Public project patterns (dev7, second round)
423
534
 
424
535
  These rules come from static runs over public pipelines (Kedro starters, MLOps
@@ -497,8 +608,9 @@ invalidate frames they receive.
497
608
 
498
609
  ## Remaining unknown behavior
499
610
 
500
- `groupby(...).apply`, `stack`/`unstack`, `crosstab`, unlisted Series methods
501
- and other unsupported calls are opaque or unmodeled. Explicit column
611
+ `groupby(...).apply`, `stack`/`unstack`, `crosstab`, unlisted Series methods,
612
+ executor fan-out (`ThreadPoolExecutor.submit`) and other unsupported calls are
613
+ opaque or unmodeled. Explicit column
502
614
  arguments can still be recorded as consumption, but unknown returned values do
503
615
  not acquire imaginary dependencies. Unknown calls can mutate arguments; affected
504
616
  state is invalidated.
@@ -114,6 +114,16 @@ unresolved facts. Reusing one after upgrading will not show newly supported edge
114
114
  - Keep helper scripts and outputs out of the analysis with `.coltraceignore`
115
115
  (gitignore syntax at the analyzed root) or `[tool.coltrace].exclude`.
116
116
 
117
+ ## Notebook output is truncated or still says "Observed names"
118
+
119
+ Use the current `examples/notebook_cell.py`. Cells copied before dev7 print every
120
+ informational note (thousands of `CT_CONDITIONAL_FLOW` lines), which notebook
121
+ platforms truncate, and label modeled names as "Observed names". The current
122
+ cell prints counts, grouped warnings, ranked candidates and, when `FUNCTION` or
123
+ `MODULE` filters exclude a name, the identities that exist without them. Keep
124
+ helper scripts (for example a one-off patch script) out of the analysis with
125
+ `.coltraceignore`.
126
+
117
127
  ## A column used inside SQL is not found
118
128
 
119
129
  Since dev8, columns read by SQL text are `table` columns and can be selected
@@ -133,6 +143,23 @@ directly. If one is still missing:
133
143
  candidate is the most likely origin. Pass `--column-id` (CLI) or `COLUMN_ID`
134
144
  (notebook cell) to choose another.
135
145
 
146
+ ## A column stops at a helper that picks the column by a parameter
147
+
148
+ Before dev10, `col = "month" if kind == "m" else "quarter"` followed by
149
+ `df[col]` invalidated the frame (`CT_DYNAMIC_COLUMN`, then `CT_OPAQUE_CALL` on
150
+ the next method). From dev10 the read is a label choice: each possible column
151
+ is read with inferred evidence and the note `CT_LABEL_CHOICE` names the site. A
152
+ label built from unknown text (user input, an f-string with unknown parts) is
153
+ still dynamic; replace it with a constant mapping or a conditional over known
154
+ labels.
155
+
156
+ ## A score computed in a row-wise `apply` has no inputs
157
+
158
+ `df.apply(f, axis=1)` is modeled when `f` resolves and returns a value or
159
+ `pd.Series({...})` with literal labels. Labels built at run time, callbacks that
160
+ write to the row, and `groupby(...).apply` stay unmodeled
161
+ (`CT_UNMODELED_RESULT`).
162
+
136
163
  ## A helper call interrupts lineage
137
164
 
138
165
  `CT_OPAQUE_CALL` marks a call ColTrace cannot resolve that receives a DataFrame
@@ -12,6 +12,7 @@ business rules.
12
12
  | `shared/config.py` | Configured column lists and default parameters |
13
13
  | `pipeline_a/extract/primary_extract.py` | f-string SQL builder, `if df.empty: return df` guard, loop over configured columns |
14
14
  | `pipeline_a/extract/quality_extract.py` | SQL assembled from f-strings with BigQuery backticks, a helper fragment, an unknown metric column and an uncalled extractor (analyzed as a template) |
15
+ | `pipeline_a/transform/combine_sources.py` | Results kept in a dict and combined with an `acc = None` accumulator and `merge` |
15
16
  | `shared/topic_changes_extract.py` | `spark.sql(build_query(days)).toPandas()` and `sdf = spark.sql(...); sdf.toPandas()` with an injected session |
16
17
  | `pipeline_a/transform/priority_rank.py` | Scalar parameters, `np.where`, `isin` masks, filters, `sort_values().head(n)` |
17
18
  | `pipeline_a/publish/serialize_webapp.py` | Projection, `to_dict(orient="records")`, a writer class |
@@ -29,7 +30,7 @@ python -m coltrace trace contact_attempts --from .coltrace/databricks/scan.json
29
30
  python -m coltrace graph priority_score --from .coltrace/databricks/scan.json --output .coltrace/databricks/priority_score.html
30
31
  ```
31
32
 
32
- Expected (dev8 also covers the follow-up report): all 15 files parse, the SQL SELECT list becomes the source of six
33
+ Expected (dev8 and dev9 also cover the follow-up reports): all 16 files parse, the SQL SELECT list becomes the source of six
33
34
  columns, and `n_contacts` flows into `volume_share`, `priority_score` and
34
35
  `published_score`, which reach the Parquet export and the serialized view. Row
35
36
  filter and sort-key columns are recorded as control, not as value inputs.
@@ -12,6 +12,7 @@ from shared.warehouse import make_run_query
12
12
  from pipeline_a.extract.primary_extract import extract_primary
13
13
  from pipeline_a.extract.quality_extract import extract_quality
14
14
  from pipeline_a.publish.serialize_webapp import serialize
15
+ from pipeline_a.transform.combine_sources import combine
15
16
  from pipeline_a.transform.priority_rank import prioritize
16
17
 
17
18
  # COMMAND ----------
@@ -32,3 +33,12 @@ payload_a = serialize(ranked_a, "/dbfs/tmp/pipeline_a.json")
32
33
  quality_a = extract_quality(run_query)
33
34
  topics_a = extract_topic_changes(spark, days=7)
34
35
  quality_a.merge(topics_a, on="session_id", how="left").to_parquet("/dbfs/tmp/quality_a.parquet")
36
+
37
+ # COMMAND ----------
38
+
39
+ results = {}
40
+ results["handled"] = quality_a[["session_id", "n_attempts"]]
41
+ results["waiting"] = extract_quality(run_query, metric="wait_seconds")[["session_id", "n_attempts"]]
42
+ combined = combine(results)
43
+ combined["attempts_total"] = combined["attempts_handled"] + combined["attempts_waiting"]
44
+ combined.to_parquet("/dbfs/tmp/attempts.parquet")
@@ -0,0 +1,9 @@
1
+ """Results collected in a dict and combined with an ``acc = None`` accumulator."""
2
+
3
+
4
+ def combine(results):
5
+ total = None
6
+ for name, part in results.items():
7
+ part = part.rename(columns={"n_attempts": f"attempts_{name}"})
8
+ total = part if total is None else total.merge(part, on="session_id", how="outer")
9
+ return total
@@ -121,6 +121,15 @@ def test_column():
121
121
  print(f"Continuing with the first identity: {selected.id}")
122
122
  except ColumnNotFoundError:
123
123
  print("Column not found: " + missing_column_message(report, COLUMN_ID or COLUMN))
124
+ if not COLUMN_ID and (FUNCTION or MODULE):
125
+ unfiltered = graph.candidates(COLUMN)
126
+ if unfiltered:
127
+ print("Without FUNCTION/MODULE filters these identities exist:")
128
+ for item in unfiltered[:10]:
129
+ print(
130
+ f" {item.id} | {item.origin} | {item.module}::{item.scope} | "
131
+ f"{item.location.path}:{item.location.line}"
132
+ )
124
133
  print("Database columns read by SQL text are selectable too (origin=table).")
125
134
  names = sorted({item.name for item in report.columns})
126
135
  print("Modeled static column names (not runtime df.columns):", ", ".join(names[:30]))