matchlab 0.1.0.dev1__tar.gz → 0.1.0.dev2__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 (213) hide show
  1. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/PKG-INFO +9 -7
  2. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/README.md +8 -6
  3. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/benchmarks/topologies.py +25 -27
  4. matchlab-0.1.0.dev1/docs/api/models.md → matchlab-0.1.0.dev2/docs/api/document.md +1 -1
  5. matchlab-0.1.0.dev2/docs/api/locations.md +46 -0
  6. matchlab-0.1.0.dev2/docs/api/models.md +46 -0
  7. matchlab-0.1.0.dev2/docs/api/resources.md +12 -0
  8. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/docs/api/sources.md +5 -1
  9. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/docs/contributing.md +9 -3
  10. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/docs/glossary.md +48 -6
  11. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/docs/guide/build-a-plan.md +72 -31
  12. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/docs/guide/evaluate.md +20 -23
  13. matchlab-0.1.0.dev2/docs/guide/install.md +21 -0
  14. matchlab-0.1.0.dev2/docs/guide/serialise.md +189 -0
  15. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/docs/index.md +6 -6
  16. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/docs/migration/matchbox-to-matchlab.md +73 -23
  17. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/mkdocs.yml +4 -1
  18. matchlab-0.1.0.dev2/src/matchlab/__init__.py +98 -0
  19. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/core/exceptions.py +4 -4
  20. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/core/hash.py +10 -8
  21. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/document.py +100 -77
  22. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/lineage.py +4 -4
  23. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/models/dedupers/base.py +9 -10
  24. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/models/dedupers/naive.py +7 -13
  25. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/models/linkers/base.py +12 -13
  26. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/models/linkers/deterministic.py +14 -22
  27. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/models/linkers/splinklinker.py +24 -31
  28. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/models/linkers/weighteddeterministic.py +9 -26
  29. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/models/models.py +76 -31
  30. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/recordstep.py +15 -6
  31. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/resolvers/__init__.py +1 -2
  32. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/resolvers/base.py +11 -10
  33. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/resolvers/components.py +7 -14
  34. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/resolvers/resolvers.py +62 -24
  35. matchlab-0.1.0.dev2/src/matchlab/resources.py +153 -0
  36. matchlab-0.1.0.dev2/src/matchlab/sources/__init__.py +31 -0
  37. matchlab-0.1.0.dev2/src/matchlab/sources/base.py +50 -0
  38. matchlab-0.1.0.dev2/src/matchlab/sources/dataframe.py +50 -0
  39. matchlab-0.1.0.dev2/src/matchlab/sources/relational.py +97 -0
  40. {matchlab-0.1.0.dev1/src/matchlab → matchlab-0.1.0.dev2/src/matchlab/sources}/sources.py +203 -60
  41. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/specs.py +17 -18
  42. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/steps.py +224 -20
  43. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/testkit/linked.py +15 -24
  44. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/testkit/matchers.py +6 -18
  45. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/testkit/sources.py +26 -23
  46. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/transformers/base.py +28 -1
  47. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/transformers/clean.py +7 -3
  48. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/transformers/group.py +2 -1
  49. matchlab-0.1.0.dev2/src/matchlab/transformers/transform.py +169 -0
  50. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab.egg-info/PKG-INFO +9 -7
  51. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab.egg-info/SOURCES.txt +14 -5
  52. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab.egg-info/scm_file_list.json +14 -5
  53. matchlab-0.1.0.dev2/src/matchlab.egg-info/scm_version.json +8 -0
  54. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab.egg-info/top_level.txt +1 -0
  55. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/conftest.py +15 -8
  56. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/core/test_hash.py +43 -50
  57. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/models/test_dedupers_deterministic.py +3 -4
  58. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/models/test_linkers_deterministic.py +12 -18
  59. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/models/test_linkers_probabilistic.py +6 -8
  60. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/plan/test_plan.py +550 -142
  61. matchlab-0.1.0.dev2/test/plan/test_steps.py +195 -0
  62. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/resolvers/test_components.py +5 -9
  63. matchlab-0.1.0.dev2/test/sources/conftest.py +39 -0
  64. matchlab-0.1.0.dev2/test/sources/test_locations.py +225 -0
  65. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/test_cli.py +5 -7
  66. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/test_document.py +83 -56
  67. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/test_lineage.py +23 -17
  68. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/test_progress.py +19 -13
  69. matchlab-0.1.0.dev2/test/test_resources.py +45 -0
  70. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/testkit/test_models.py +4 -2
  71. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/testkit/test_sources.py +6 -3
  72. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/transformers/test_transform.py +59 -1
  73. matchlab-0.1.0.dev1/docs/api/locations.md +0 -12
  74. matchlab-0.1.0.dev1/docs/guide/install.md +0 -52
  75. matchlab-0.1.0.dev1/src/matchlab/__init__.py +0 -55
  76. matchlab-0.1.0.dev1/src/matchlab/locations.py +0 -347
  77. matchlab-0.1.0.dev1/src/matchlab/transformers/transform.py +0 -101
  78. matchlab-0.1.0.dev1/src/matchlab.egg-info/scm_version.json +0 -8
  79. matchlab-0.1.0.dev1/test/locations/conftest.py +0 -88
  80. matchlab-0.1.0.dev1/test/locations/test_locations.py +0 -294
  81. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/.github/actions/setup-toolchain/action.yml +0 -0
  82. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/.github/dependabot.yml +0 -0
  83. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/.github/pull_request_template.md +0 -0
  84. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/.github/skills/lucid-docs/README.md +0 -0
  85. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/.github/skills/lucid-docs/SKILL.md +0 -0
  86. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/.github/skills/lucid-docs/readability.py +0 -0
  87. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/.github/workflows/cd.yml +0 -0
  88. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/.github/workflows/ci.yml +0 -0
  89. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/.github/workflows/codeql.yml +0 -0
  90. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/.gitignore +0 -0
  91. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/.pre-commit-config.yaml +0 -0
  92. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/.vscode/launch.json +0 -0
  93. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/.vscode/settings.json +0 -0
  94. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/CODEOWNERS +0 -0
  95. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/LICENSE +0 -0
  96. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/benchmarks/README.md +0 -0
  97. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/benchmarks/__init__.py +0 -0
  98. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/benchmarks/__main__.py +0 -0
  99. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/benchmarks/cases.py +0 -0
  100. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/benchmarks/data.py +0 -0
  101. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/benchmarks/measure.py +0 -0
  102. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/benchmarks/measurements.py +0 -0
  103. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/benchmarks/report.py +0 -0
  104. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/benchmarks/results/.gitkeep +0 -0
  105. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/benchmarks/runner.py +0 -0
  106. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/docs/api/adapters.md +0 -0
  107. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/docs/api/core/exceptions.md +0 -0
  108. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/docs/api/core/hash.md +0 -0
  109. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/docs/api/core/resolver_output.md +0 -0
  110. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/docs/api/core/schemas.md +0 -0
  111. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/docs/api/core/transform.md +0 -0
  112. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/docs/api/eval-judgements.md +0 -0
  113. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/docs/api/eval-metrics.md +0 -0
  114. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/docs/api/eval-sampling.md +0 -0
  115. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/docs/api/eval-tui.md +0 -0
  116. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/docs/api/lineage.md +0 -0
  117. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/docs/api/progress.md +0 -0
  118. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/docs/api/recordstep.md +0 -0
  119. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/docs/api/resolvers.md +0 -0
  120. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/docs/api/specs.md +0 -0
  121. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/docs/api/steps.md +0 -0
  122. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/docs/api/testkit.md +0 -0
  123. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/docs/api/transformers.md +0 -0
  124. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/docs/assets/matchbox-icon-dark.png +0 -0
  125. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/docs/assets/matchbox-icon.svg +0 -0
  126. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/docs/assets/matchbox-logo-dark.svg +0 -0
  127. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/docs/assets/matchbox-logo-light.svg +0 -0
  128. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/docs/guide/query.md +0 -0
  129. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/docs/stylesheets/extra.css +0 -0
  130. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/docs/use-cases.md +0 -0
  131. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/docs/why-matchlab.md +0 -0
  132. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/justfile +0 -0
  133. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/personal-data-exclusions.txt +0 -0
  134. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/pyproject.toml +0 -0
  135. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/scripts/test_eval.py +0 -0
  136. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/setup.cfg +0 -0
  137. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/adapters/__init__.py +0 -0
  138. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/adapters/base.py +0 -0
  139. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/adapters/duckdb.py +0 -0
  140. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/cli.py +0 -0
  141. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/core/__init__.py +0 -0
  142. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/core/dataframes.py +0 -0
  143. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/core/dsu.py +0 -0
  144. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/core/kinds.py +0 -0
  145. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/core/logging.py +0 -0
  146. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/core/resolver_output.py +0 -0
  147. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/core/schemas.py +0 -0
  148. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/core/sql.py +0 -0
  149. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/eval/__init__.py +0 -0
  150. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/eval/judgements.py +0 -0
  151. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/eval/metrics.py +0 -0
  152. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/eval/samples.py +0 -0
  153. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/eval/tui/__init__.py +0 -0
  154. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/eval/tui/app.py +0 -0
  155. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/eval/tui/modals.py +0 -0
  156. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/eval/tui/styles.tcss +0 -0
  157. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/eval/tui/widgets/__init__.py +0 -0
  158. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/eval/tui/widgets/assignment.py +0 -0
  159. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/eval/tui/widgets/styling.py +0 -0
  160. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/eval/tui/widgets/table.py +0 -0
  161. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/models/__init__.py +0 -0
  162. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/models/comparison.py +0 -0
  163. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/models/dedupers/__init__.py +0 -0
  164. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/models/linkers/__init__.py +0 -0
  165. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/progress.py +0 -0
  166. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/testkit/__init__.py +0 -0
  167. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/testkit/_generate.py +0 -0
  168. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/testkit/compare.py +0 -0
  169. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/testkit/entities.py +0 -0
  170. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/testkit/features.py +0 -0
  171. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/testkit/models.py +0 -0
  172. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/transformers/__init__.py +0 -0
  173. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/transformers/explode.py +0 -0
  174. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab/transformers/select.py +0 -0
  175. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab.egg-info/dependency_links.txt +0 -0
  176. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab.egg-info/entry_points.txt +0 -0
  177. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/src/matchlab.egg-info/requires.txt +0 -0
  178. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/README.md +0 -0
  179. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/__init__.py +0 -0
  180. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/adapters/__init__.py +0 -0
  181. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/adapters/conftest.py +0 -0
  182. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/adapters/test_base.py +0 -0
  183. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/adapters/test_contract.py +0 -0
  184. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/adapters/test_duckdb.py +0 -0
  185. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/core/__init__.py +0 -0
  186. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/core/test_dsu.py +0 -0
  187. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/core/test_eval.py +0 -0
  188. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/core/test_resolver_output.py +0 -0
  189. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/eval/__init__.py +0 -0
  190. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/eval/test_samples.py +0 -0
  191. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/eval/tui/__init__.py +0 -0
  192. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/eval/tui/test_app.py +0 -0
  193. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/eval/tui/test_widgets.py +0 -0
  194. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/models/__init__.py +0 -0
  195. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/models/test_comparison.py +0 -0
  196. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/models/test_models.py +0 -0
  197. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/plan/__init__.py +0 -0
  198. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/plan/test_ground_truth.py +0 -0
  199. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/resolvers/__init__.py +0 -0
  200. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/resolvers/test_resolvers.py +0 -0
  201. {matchlab-0.1.0.dev1/test/locations → matchlab-0.1.0.dev2/test/sources}/__init__.py +0 -0
  202. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/test_benchmarks.py +0 -0
  203. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/testkit/__init__.py +0 -0
  204. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/testkit/test_entities.py +0 -0
  205. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/testkit/test_linked.py +0 -0
  206. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/testkit/test_scores.py +0 -0
  207. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/transformers/__init__.py +0 -0
  208. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/transformers/test_clean.py +0 -0
  209. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/transformers/test_explode.py +0 -0
  210. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/transformers/test_group.py +0 -0
  211. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/test/transformers/test_select.py +0 -0
  212. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/trufflehog-exclude.txt +0 -0
  213. {matchlab-0.1.0.dev1 → matchlab-0.1.0.dev2}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matchlab
3
- Version: 0.1.0.dev1
3
+ Version: 0.1.0.dev2
4
4
  Summary: A local-first library for building, running and evaluating entity resolution pipelines.
5
5
  Author: Department for Business and Trade
6
6
  Project-URL: Documentation, https://uktrade.github.io/matchlab/
@@ -40,19 +40,21 @@ Dynamic: license-file
40
40
  Record matching is a chore. matchlab makes it a pipeline you can build, run, query and measure — on your machine, against your warehouse, with nothing to deploy.
41
41
 
42
42
  ```python
43
- from matchlab import Source
44
- from matchlab.models.dedupers import NaiveDeduper
43
+ import matchlab as mb
45
44
 
46
- companies = Source(
47
- location=warehouse,
45
+ companies = mb.read_database(
48
46
  name="crn",
49
- extract_transform="select pk, company, town from companies",
47
+ sql="select pk, company, town from companies",
48
+ client=warehouse,
50
49
  key_field="pk",
51
50
  )
52
51
 
53
52
  entities = (
54
53
  companies.clean({"name": "lower(crn_company)"})
55
- .dedupe(model_class=NaiveDeduper, model_settings={"unique_fields": ["name"]})
54
+ .dedupe(
55
+ model_class=mb.NaiveDeduper,
56
+ model_settings={"unique_fields": ["name"]},
57
+ )
56
58
  .resolve()
57
59
  .collect()
58
60
  )
@@ -5,19 +5,21 @@
5
5
  Record matching is a chore. matchlab makes it a pipeline you can build, run, query and measure — on your machine, against your warehouse, with nothing to deploy.
6
6
 
7
7
  ```python
8
- from matchlab import Source
9
- from matchlab.models.dedupers import NaiveDeduper
8
+ import matchlab as mb
10
9
 
11
- companies = Source(
12
- location=warehouse,
10
+ companies = mb.read_database(
13
11
  name="crn",
14
- extract_transform="select pk, company, town from companies",
12
+ sql="select pk, company, town from companies",
13
+ client=warehouse,
15
14
  key_field="pk",
16
15
  )
17
16
 
18
17
  entities = (
19
18
  companies.clean({"name": "lower(crn_company)"})
20
- .dedupe(model_class=NaiveDeduper, model_settings={"unique_fields": ["name"]})
19
+ .dedupe(
20
+ model_class=mb.NaiveDeduper,
21
+ model_settings={"unique_fields": ["name"]},
22
+ )
21
23
  .resolve()
22
24
  .collect()
23
25
  )
@@ -29,9 +29,7 @@ from typing import cast
29
29
 
30
30
  from sqlalchemy import create_engine
31
31
 
32
- from matchlab import Model, RecordStep, RelationalDBLocation, Resolver, Source
33
- from matchlab.models.dedupers import NaiveDeduper
34
- from matchlab.models.linkers import DeterministicLinker
32
+ import matchlab as mb
35
33
 
36
34
  # Strip case, whitespace, punctuation and company suffixes, so the cosmetic variation
37
35
  # the generator plants is absorbed and matching has something to agree on.
@@ -72,27 +70,25 @@ class Topology(StrEnum):
72
70
  }[self]
73
71
 
74
72
 
75
- def declare(path: Path, names: Sequence[str]) -> list[Source]:
73
+ def declare(path: Path, names: Sequence[str]) -> list[mb.Source]:
76
74
  """Declare one source per table in a generated warehouse.
77
75
 
78
76
  One engine for all of them, because they are one database and a connection per
79
77
  source would measure SQLite's connection handling rather than matchlab's.
80
78
  """
81
- location = RelationalDBLocation(
82
- name="warehouse", client=create_engine(f"sqlite:///{path}")
83
- )
79
+ client = create_engine(f"sqlite:///{path}")
84
80
  return [
85
- Source(
86
- location=location,
87
- name=name,
88
- extract_transform=f"select key, name, postcode from {name}",
81
+ mb.read_database(
82
+ name,
83
+ sql=f"select key, name, postcode from {name}",
84
+ client=client,
89
85
  key_field="key",
90
86
  )
91
87
  for name in names
92
88
  ]
93
89
 
94
90
 
95
- def _cleaning(sources: Sequence[Source]) -> dict[str, str]:
91
+ def _cleaning(sources: Sequence[mb.Source]) -> dict[str, str]:
96
92
  """Cleaning expressions that work across however many sources a record step reads.
97
93
 
98
94
  A multi-source record step concatenates the extracts diagonally, so each source's
@@ -114,8 +110,8 @@ def _cleaning(sources: Sequence[Source]) -> dict[str, str]:
114
110
 
115
111
 
116
112
  def _record_step(
117
- sources: Sequence[Source], resolver: Resolver | None = None
118
- ) -> RecordStep:
113
+ sources: Sequence[mb.Source], resolver: mb.Resolver | None = None
114
+ ) -> mb.RecordStep:
119
115
  """A cleaned record step of these sources, read through a resolver if given one.
120
116
 
121
117
  A resolver is itself a record step, read at `id`=entity root, so passing one is the
@@ -124,11 +120,11 @@ def _record_step(
124
120
  one is given. Without a resolver, there is a single source, read directly at
125
121
  `id`=leaf.
126
122
  """
127
- base: RecordStep = resolver if resolver is not None else sources[0]
123
+ base: mb.RecordStep = resolver if resolver is not None else sources[0]
128
124
  return base.clean(_cleaning(sources))
129
125
 
130
126
 
131
- def _through(resolver: Resolver, source: Source) -> RecordStep:
127
+ def _through(resolver: mb.Resolver, source: mb.Source) -> mb.RecordStep:
132
128
  """One source's records, per entity, read through a multi-source resolver.
133
129
 
134
130
  The resolver's record step carries every source's rows diagonally, so this
@@ -146,23 +142,23 @@ def _through(resolver: Resolver, source: Source) -> RecordStep:
146
142
  )
147
143
 
148
144
 
149
- def _dedupe(record_step: RecordStep) -> Model:
145
+ def _dedupe(record_step: mb.RecordStep) -> mb.Model:
150
146
  """Deduplicate a cleaned record step. Same name and postcode is one entity."""
151
147
  return record_step.dedupe(
152
- model_class=NaiveDeduper, model_settings={"unique_fields": UNIQUE_FIELDS}
148
+ model_class=mb.NaiveDeduper, model_settings={"unique_fields": UNIQUE_FIELDS}
153
149
  )
154
150
 
155
151
 
156
- def _link(left: RecordStep, right: RecordStep) -> Model:
152
+ def _link(left: mb.RecordStep, right: mb.RecordStep) -> mb.Model:
157
153
  """Link two cleaned record steps, on the same rule the deduper uses."""
158
154
  return left.link(
159
155
  right,
160
- model_class=DeterministicLinker,
156
+ model_class=mb.DeterministicLinker,
161
157
  model_settings={"comparisons": COMPARISON},
162
158
  )
163
159
 
164
160
 
165
- def build(topology: Topology, sources: Sequence[Source]) -> Resolver:
161
+ def build(topology: Topology, sources: Sequence[mb.Source]) -> mb.Resolver:
166
162
  """Build a plan of this shape over these sources.
167
163
 
168
164
  Args:
@@ -193,22 +189,24 @@ def build(topology: Topology, sources: Sequence[Source]) -> Resolver:
193
189
 
194
190
 
195
191
  # Which links a star-shaped plan builds, given its record steps.
196
- Pairs = Callable[[Sequence[RecordStep]], list[tuple[RecordStep, RecordStep]]]
192
+ Pairs = Callable[[Sequence[mb.RecordStep]], list[tuple[mb.RecordStep, mb.RecordStep]]]
197
193
 
198
194
 
199
- def _spokes(record_steps: Sequence[RecordStep]) -> list[tuple[RecordStep, RecordStep]]:
195
+ def _spokes(
196
+ record_steps: Sequence[mb.RecordStep],
197
+ ) -> list[tuple[mb.RecordStep, mb.RecordStep]]:
200
198
  """Every (hub, spoke) pair. `n-1` links, and the hub in all of them."""
201
199
  return [(record_steps[0], spoke) for spoke in record_steps[1:]]
202
200
 
203
201
 
204
202
  def _every_pair(
205
- record_steps: Sequence[RecordStep],
206
- ) -> list[tuple[RecordStep, RecordStep]]:
203
+ record_steps: Sequence[mb.RecordStep],
204
+ ) -> list[tuple[mb.RecordStep, mb.RecordStep]]:
207
205
  """Every pair of record steps. `n(n-1)/2` links, and no record step privileged."""
208
206
  return list(combinations(record_steps, 2))
209
207
 
210
208
 
211
- def _star(sources: Sequence[Source], pairs: Pairs) -> Resolver:
209
+ def _star(sources: Sequence[mb.Source], pairs: Pairs) -> mb.Resolver:
212
210
  """Dedupe everything into one resolver output, then link the pairs `pairs` chooses.
213
211
 
214
212
  `HUB` and `MESH` differ only in which pairs get linked, so they share everything
@@ -224,7 +222,7 @@ def _star(sources: Sequence[Source], pairs: Pairs) -> Resolver:
224
222
  return links[0].resolve(*links[1:])
225
223
 
226
224
 
227
- def _chain(sources: Sequence[Source]) -> Resolver:
225
+ def _chain(sources: Sequence[mb.Source]) -> mb.Resolver:
228
226
  """Add one source per level, each level built on the resolver output below it.
229
227
 
230
228
  Where `MESH` is one resolver output over many links, this is many resolver outputs
@@ -1,4 +1,4 @@
1
- ::: matchlab.models
1
+ ::: matchlab.document
2
2
  options:
3
3
  show_root_heading: true
4
4
  show_root_full_path: true
@@ -0,0 +1,46 @@
1
+ # Locations
2
+
3
+ The methodologies a [source](sources.md) reads through. A source wraps exactly one location, named by `location_class` and configured by `location_settings`, with its client or frame passed separately as a [resource](resources.md).
4
+
5
+ ::: matchlab.sources.base
6
+ options:
7
+ show_root_heading: true
8
+ show_root_full_path: true
9
+ members_order: source
10
+ show_if_no_docstring: true
11
+ docstring_style: google
12
+ show_signature_annotations: true
13
+ separate_signature: true
14
+ filters:
15
+ - "!^[A-Z]$"
16
+ - "!^_"
17
+
18
+ ## Relational databases
19
+
20
+ ::: matchlab.sources.relational
21
+ options:
22
+ show_root_heading: true
23
+ show_root_full_path: true
24
+ members_order: source
25
+ show_if_no_docstring: true
26
+ docstring_style: google
27
+ show_signature_annotations: true
28
+ separate_signature: true
29
+ filters:
30
+ - "!^[A-Z]$"
31
+ - "!^_"
32
+
33
+ ## Dataframes
34
+
35
+ ::: matchlab.sources.dataframe
36
+ options:
37
+ show_root_heading: true
38
+ show_root_full_path: true
39
+ members_order: source
40
+ show_if_no_docstring: true
41
+ docstring_style: google
42
+ show_signature_annotations: true
43
+ separate_signature: true
44
+ filters:
45
+ - "!^[A-Z]$"
46
+ - "!^_"
@@ -0,0 +1,46 @@
1
+ # Models
2
+
3
+ The `Model` step, and the methodologies it runs. A model wraps exactly one [deduper](#matchlab.models.dedupers) or [linker](#matchlab.models.linkers), named by `model_class` and configured by `model_settings`.
4
+
5
+ ::: matchlab.models
6
+ options:
7
+ show_root_heading: true
8
+ show_root_full_path: true
9
+ members_order: source
10
+ show_if_no_docstring: true
11
+ docstring_style: google
12
+ show_signature_annotations: true
13
+ separate_signature: true
14
+ filters:
15
+ - "!^[A-Z]$"
16
+ - "!^_"
17
+
18
+ ## Dedupers
19
+
20
+ ::: matchlab.models.dedupers
21
+ options:
22
+ show_root_heading: true
23
+ show_root_full_path: true
24
+ members_order: source
25
+ show_if_no_docstring: true
26
+ docstring_style: google
27
+ show_signature_annotations: true
28
+ separate_signature: true
29
+ filters:
30
+ - "!^[A-Z]$"
31
+ - "!^_"
32
+
33
+ ## Linkers
34
+
35
+ ::: matchlab.models.linkers
36
+ options:
37
+ show_root_heading: true
38
+ show_root_full_path: true
39
+ members_order: source
40
+ show_if_no_docstring: true
41
+ docstring_style: google
42
+ show_signature_annotations: true
43
+ separate_signature: true
44
+ filters:
45
+ - "!^[A-Z]$"
46
+ - "!^_"
@@ -0,0 +1,12 @@
1
+ ::: matchlab.resources
2
+ options:
3
+ show_root_heading: true
4
+ show_root_full_path: true
5
+ members_order: source
6
+ show_if_no_docstring: true
7
+ docstring_style: google
8
+ show_signature_annotations: true
9
+ separate_signature: true
10
+ filters:
11
+ - "!^[A-Z]$"
12
+ - "!^_"
@@ -1,4 +1,8 @@
1
- ::: matchlab.sources
1
+ # Sources
2
+
3
+ The `Source` step, and the shorthands that build one. A source reads rows through a [location](locations.md) and content-addresses them, so it is where data enters a plan.
4
+
5
+ ::: matchlab.sources.sources
2
6
  options:
3
7
  show_root_heading: true
4
8
  show_root_full_path: true
@@ -47,8 +47,7 @@ The whole suite runs on Python alone. Storage uses DuckDB in memory, and warehou
47
47
  just test
48
48
  ```
49
49
 
50
- No container is needed, including for the tests that compare SQL dialects. `validate_extract_transform` only asks a client what dialect it speaks, and SQLAlchemy answers that without connecting, so those tests use a Postgres-dialect engine with no driver behind it.
51
-
50
+ No container is needed: every warehouse test runs against SQLite, over a SQLAlchemy engine or an ADBC connection.
52
51
  If you want a real warehouse to point matchlab at while developing, bring up whatever database you like and pass its client to a `Location`. matchlab has no opinion about where it runs, and ships nothing to manage one.
53
52
 
54
53
  ## Documentation
@@ -88,8 +87,15 @@ When contributing to matchlab and its associated repos, we try to follow consist
88
87
  New plan steps subclass [`Step`](api/steps.md). A step must:
89
88
 
90
89
  * Hold references to its inputs, and to nothing downstream of it
91
- * Return a stable `_config_key()` covering everything that changes its output, so caching is correct
90
+ * Return a stable `_spec_key()` covering everything that changes its output, so caching is correct
92
91
  * Do all its work in `_execute()`, reading inputs from the adapter by fingerprint
92
+ * Settle in `__init__`, listing everything its spec is built from in `_READ_ONLY` so a later assignment raises
93
+
94
+ That last rule is what keeps a step's configuration, the thing it runs, and its cached artifact from disagreeing. A step builds its methodology from its settings at construction, so an assignment afterwards moves one attribute and nothing derived from it: set `model_class` and the instance still runs the old class, leaving `spec` naming one configuration while `_execute` runs another. The cache leans on the same guarantee from the other side — `_ensure` short-circuits on a stored `_fp` *because* a settled step cannot have changed, so it would hand back the old artifact without consulting the spec at all. Validate first, then write through `_set()`, which is the only thing that gets past the guard.
95
+
96
+ Derive anything you can rather than storing it. `Transform.transformer_class` and `Model.model_type` are properties over the attributes they describe, so they cannot drift from them.
97
+
98
+ A methodology must not write into its own settings either, for the same reason: those settings are hashed into the owning step's fingerprint. Copy at the boundary if a library you wrap mutates what you hand it, as `SplinkLinker.prepare` does.
93
99
 
94
100
  ### Adapters
95
101
 
@@ -50,13 +50,23 @@ The 32-byte SHA-256 digest that keys a step's stored [artifact](#artifact). Two
50
50
 
51
51
  A person's decision that one reviewed cluster's records do, or do not, describe the same [entity](#entity), recorded by matchlab's evaluation tools and scored against a [resolver](#resolver)'s clusters. A judgement is anchored to the content a reviewer was shown, not to any record's key, which is why a [leaf](#leaf) ID, a hash of content, rather than a key, is what a judgement endorses or rejects.
52
52
 
53
+ ## Key
54
+
55
+ A record's identifier at the origin, in the [source](#source)'s own data. Its column is named by `key_field`, which defaults to `id`.
56
+
57
+ A key is not the `id` a [record step](#record-step) carries. That one is a [leaf](#leaf) or a [root](#root), derived by hashing content when the plan is collected. The two never collide, because every column an [extract](#extract) returns is [qualified](#qualify) with the source name first, so a key named `id` reaches a record step as `crn_id`. `leaves()` returns both, under the names `key` and `leaf`.
58
+
59
+ A key is read as a string whatever the location stores it as, and a null key is refused. `get_lookup()` returns keys, which is how a plan's result joins back to your own tables.
60
+
61
+ Distinct from an [answer key](#answer-key), which is a [testkit](#testkit) term that shares only the word.
62
+
53
63
  ## Label
54
64
 
55
65
  A pointer from a name someone chose to a [resolver](#resolver)'s [fingerprint](#fingerprint), created by [publishing](#publish) a resolver's output. A label belongs to the store, not the plan. It can be re-aimed at a different resolver, whereas a source's `name` is part of that source's own output and never moves.
56
66
 
57
67
  ## Leaf
58
68
 
59
- The stable ID of one record, a hash of its content rather than its key. Identity coming from content rather than key is why a leaf ID changes only when the underlying data does, which is the basis matchlab anchors evaluation judgements to. Reading a source or record step directly, without going through a resolver, exposes the leaf as the `id` column.
69
+ The stable ID of one record, a hash of its content rather than its [key](#key). Identity coming from content rather than key is why a leaf ID changes only when the underlying data does, which is the basis matchlab anchors evaluation judgements to. Reading a source or record step directly, without going through a resolver, exposes the leaf as the `id` column.
60
70
 
61
71
  ## Linker
62
72
 
@@ -64,7 +74,11 @@ A [methodology](#methodology) that finds candidate matches between two [record s
64
74
 
65
75
  ## Location
66
76
 
67
- matchlab's handle on the warehouse a [source](#source) reads from: a database, plus the client that reaches it. `RelationalDBLocation` is the reference implementation, wrapping a SQLAlchemy or ADBC client. The warehouse is the external database itself; the location is the handle, and it is where a source's rows come *from* — distinct from the [store](#store), where a plan's [artifacts](#artifact) are kept once collected. A location resolves nothing and stores nothing; it only answers a query.
77
+ A generic way of getting a [source](#source)'s rows, and a [methodology](#methodology) like any other. `RelationalDB` runs a query. `DataFrame` reads a frame already in memory. A custom one subclasses `Location` and registers with `add_location_class`.
78
+
79
+ A location is where a source's rows come from. Do not confuse it with the [store](#store), which keeps a plan's [artifacts](#artifact) once you collect it. A location only reads.
80
+
81
+ Its [settings](#settings) are hashed into the source's [fingerprint](#fingerprint), so editing a query re-runs the source. Its [resources](#resource) are not hashed, so renaming a warehouse changes nothing.
68
82
 
69
83
  ## Matcher
70
84
 
@@ -78,7 +92,11 @@ The guarantee that a [resolver](#resolver)'s stored output carries forward every
78
92
 
79
93
  ## Methodology
80
94
 
81
- The matching algorithm a [model](#model) or [resolver](#resolver) step runs. Models run `NaiveDeduper`, `DeterministicLinker`, `SplinkLinker`, or `WeightedDeterministicLinker`. Resolvers run connected components. A model wraps one methodology, chosen by `model_class`, using a [deduper](#deduper) to match within one record step or a [linker](#linker) to match between two. Swapping the methodology only means changing `model_class` (or `resolver_class`), never restructuring the plan around it.
95
+ The pluggable class a [step](#step) runs. Every kind of step has one. A [source](#source) runs a [location](#location), a [transform](#transform) runs a transformer, a [model](#model) runs a [deduper](#deduper) or a [linker](#linker), and a [resolver](#resolver) runs a resolver method.
96
+
97
+ Every step is built the same way. It names its methodology in a `*_class` argument and configures it with `*_settings`. Anything that cannot be serialised goes in `*_resources`. Swapping a methodology means changing `*_class`, never restructuring the plan around it.
98
+
99
+ Each kind keeps its own registry, so a custom methodology can be named in a [plan document](#plan-document). Register with `add_location_class`, `add_transformer_class`, `add_model_class`, or `add_resolver_class`.
82
100
 
83
101
  ## Model
84
102
 
@@ -88,13 +106,19 @@ A step that scores candidate matches by running one [methodology](#methodology).
88
106
 
89
107
  A tree of [steps](#step). Each step holds a reference to its own inputs, so the step you are holding is the pipeline. There is no separate object to register steps with. Nothing runs until you [collect](#collect) it.
90
108
 
109
+ ## Plan document
110
+
111
+ A serialisable description of a [plan](#plan), dumped with `dump()` and loaded back with `load()`. Called a **document** for short, which is what the rest of these docs call it. It is JSON, derived from a plan rather than hand-authored, which is why its nodes refer to each other by [position](#position) rather than by any name.
112
+
113
+ A document carries each step's [settings](#settings) and the edges between steps. It carries no code, no [label](#label), no data, and no [resource](#resource), only the name each resource was given. A plan rebuilt from a document [fingerprints](#fingerprint) identically to the plan it came from, given the same data, so a [store](#store) holding the original's [artifacts](#artifact) serves them rather than redoing the work. `PlanDocument` is the class.
114
+
91
115
  ## Position
92
116
 
93
117
  Where something falls in an ordered sequence, used instead of a name. The repository uses the word for two different sequences, and the numbers do not agree with each other.
94
118
 
95
119
  A step's place in `collect()`'s run order is one sense. `draw()` shows it in brackets, and logs quote it (`[step 5]`). Steps have no names, so position is how a log line, a drawn tree, and `plan.lineage()` all refer to the same step. Positions are relative to the step a plan or drawing starts from, so a sub-plan numbers its steps differently from the full plan it came from.
96
120
 
97
- A setting that must point at one of a step's own inputs uses the other sense. It names that input by its index among the step's own inputs, not by that input's place in the whole plan. `ComponentsSettings.thresholds` keys a per-model threshold this way. A model can sit at plan position 5 while still being resolver-input position 0.
121
+ A setting that must point at one of a step's own inputs uses the other sense. It names that input by its index among the step's own inputs, not by that input's place in the whole plan. `Components.thresholds` keys a per-model threshold this way. A model can sit at plan position 5 while still being resolver-input position 0.
98
122
 
99
123
  ## Prune
100
124
 
@@ -108,7 +132,7 @@ Re-publishing the same label at the same resolver output is a no-op. Aiming an e
108
132
 
109
133
  ## Qualify
110
134
 
111
- Prefix a column name with its [source](#source)'s name (`first_name` becomes `crn_first_name`), so the same field from two sources can sit side by side without colliding. A qualified column's prefix must parse as a valid identifier, which is why a source's `name` is restricted to safe characters.
135
+ Prefix a column name with its [source](#source)'s name (`first_name` becomes `crn_first_name`), so the same field from two sources can sit side by side without colliding. A qualified column's prefix must parse as a valid identifier, which is why a source's `name` is restricted to safe characters, and it is why two different sources in one plan cannot share a name.
112
136
 
113
137
  ## Record step
114
138
 
@@ -124,13 +148,31 @@ A step that collapses a model's scored edges into clusters, one per [entity](#en
124
148
 
125
149
  Call this a **Resolver**, or its **merge-forwarded Resolver output** if you mean the stored table specifically. Don't call it "a resolution". That noun has been retired.
126
150
 
151
+ ## Resource
152
+
153
+ A named object a node needs to run that can't be serialised. Pass one in a step's `*_resources` argument, never among its [settings](#settings). Wrap it as `Resource("warehouse", engine)` to give it the name a [plan document](#plan-document) records. Supply it again when that document is loaded.
154
+
155
+ For sources, resources are the data-generating mechanism, which affects the [fingerprint](#fingerprint). For all other types of node, resources cannot influence the output of the node because they're ignored by the fingerprint.
156
+
157
+ A location or methodology marks its resource fields `FromResources`. Every other field is a setting. Passing a field in the wrong argument raises an error that names the field.
158
+
159
+ Sharing a resource name means sharing a resource. Two steps of different kinds can't share a resource.
160
+
127
161
  ## Root
128
162
 
129
163
  The ID of a cluster a resolver produces, a hash of the sorted set of [leaf](#leaf) IDs it contains. Two runs that produce the same clustering produce the same root ID, whatever order the underlying algorithm found its clusters in. Reading a [record step](#record-step) through a resolver exposes the root as the `id` column, so several records can share one `id`.
130
164
 
165
+ ## Settings
166
+
167
+ A step's serialisable configuration. Passed in a step's `*_settings` argument, carried in a plan document, and hashed into the step's [fingerprint](#fingerprint).
168
+
169
+ Hashing is what makes settings the opposite of a [resource](#resource): **editing settings re-runs the step and everything below it**, whether or not the output would differ.
170
+
171
+ Every field of a location or methodology is a setting unless the class marks it a resource.
172
+
131
173
  ## Source
132
174
 
133
- The warehouse query a plan starts from, plus the column that keys it. Every column the query returns is part of a record's identity, so two rows are the same record exactly when the query returns identical values for both.
175
+ The leaf a plan starts from: a [location](#location) to read, plus the column that [keys](#key) it. Every column the location returns is part of a record's identity, so two rows are the same record exactly when it returns identical values for both. Build one with `read_database` or `read_dataframe`.
134
176
 
135
177
  ## Step
136
178