linkedparticles-core 1.149.0__tar.gz → 1.151.18__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 (182) hide show
  1. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/PKG-INFO +3 -3
  2. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/README.md +1 -1
  3. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/core/conflict_resolution.py +166 -2
  4. linkedparticles_core-1.151.18/particles/core/conflict_review.py +310 -0
  5. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/core/equivalence.py +46 -0
  6. linkedparticles_core-1.151.18/particles/core/provenance.py +38 -0
  7. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/extraction/journal.py +2 -2
  8. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/pyproject.toml +5 -3
  9. linkedparticles_core-1.151.18/tests/test_core_provenance.py +81 -0
  10. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/LICENSE +0 -0
  11. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/NOTICE +0 -0
  12. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/artifacts/conformance/README.md +0 -0
  13. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/artifacts/conformance/profile.yaml +0 -0
  14. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/artifacts/conformance/similarity_vectors.json +0 -0
  15. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/artifacts/schemas/context.jsonld +0 -0
  16. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/artifacts/schemas/interchange.schema.json +0 -0
  17. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/artifacts/schemas/particle.schema.json +0 -0
  18. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/artifacts/schemas/shacl/CorpusSnapshotShape.ttl +0 -0
  19. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/artifacts/schemas/shacl/ParticleShape.ttl +0 -0
  20. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/artifacts/schemas/shacl/ProvenanceChainShape.ttl +0 -0
  21. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/artifacts/schemas/shacl/SubjectShape.ttl +0 -0
  22. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/artifacts/schemas/shacl/TrustStatementShape.ttl +0 -0
  23. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/artifacts/schemas/trust_lens.schema.json +0 -0
  24. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/constraints.txt +0 -0
  25. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/__init__.py +0 -0
  26. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/config.py +0 -0
  27. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/conformance/__init__.py +0 -0
  28. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/conformance/_resources.py +0 -0
  29. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/conformance/contract.py +0 -0
  30. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/conformance/fixtures.py +0 -0
  31. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/conformance/jsonschema.py +0 -0
  32. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/conformance/profile.py +0 -0
  33. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/conformance/runner.py +0 -0
  34. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/conformance/shacl.py +0 -0
  35. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/conformance/types.py +0 -0
  36. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/conformance/validator.py +0 -0
  37. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/core/__init__.py +0 -0
  38. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/core/_resources.py +0 -0
  39. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/core/cascade_gate.py +0 -0
  40. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/core/claims.py +0 -0
  41. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/core/duplicate_key.py +0 -0
  42. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/core/fingerprint.py +0 -0
  43. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/core/granularity.py +0 -0
  44. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/core/jsonld_context.py +0 -0
  45. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/core/observer_scope.py +0 -0
  46. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/core/schema.py +0 -0
  47. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/core/scoring/__init__.py +0 -0
  48. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/core/scoring/confidence.py +0 -0
  49. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/core/scoring/decay.py +0 -0
  50. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/core/scoring/relevance.py +0 -0
  51. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/core/scoring/utility.py +0 -0
  52. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/core/stance.py +0 -0
  53. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/core/status.py +0 -0
  54. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/embeddings.py +0 -0
  55. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/extraction/__init__.py +0 -0
  56. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/extraction/calibration.py +0 -0
  57. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/extraction/docstrings.py +0 -0
  58. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/extraction/general.py +0 -0
  59. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/extraction/github/__init__.py +0 -0
  60. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/extraction/github/_shared.py +0 -0
  61. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/extraction/github/gist.py +0 -0
  62. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/extraction/github/pages.py +0 -0
  63. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/extraction/github/repo.py +0 -0
  64. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/extraction/hackernews.py +0 -0
  65. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/extraction/incremental.py +0 -0
  66. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/extraction/mastodon.py +0 -0
  67. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/extraction/mcp_memory.py +0 -0
  68. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/extraction/migration_preview.py +0 -0
  69. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/extraction/nomisma.py +0 -0
  70. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/extraction/numista/__init__.py +0 -0
  71. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/extraction/numista/_shared.py +0 -0
  72. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/extraction/numista/coin.py +0 -0
  73. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/extraction/numista/issuer.py +0 -0
  74. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/extraction/numista/listing.py +0 -0
  75. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/extraction/polarity.py +0 -0
  76. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/extraction/property_keys.py +0 -0
  77. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/extraction/rdf.py +0 -0
  78. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/extraction/reddit.py +0 -0
  79. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/extraction/registry.py +0 -0
  80. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/extraction/scope.py +0 -0
  81. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/extraction/structure.py +0 -0
  82. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/extraction/subject_gate.py +0 -0
  83. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/extraction/subject_scope.py +0 -0
  84. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/extraction/taxonomy.py +0 -0
  85. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/extraction/tool_turns.py +0 -0
  86. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/extraction/trust_lens.py +0 -0
  87. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/extraction/wikidata.py +0 -0
  88. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/http.py +0 -0
  89. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/interchange/__init__.py +0 -0
  90. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/interchange/codec.py +0 -0
  91. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/interchange/jsonl.py +0 -0
  92. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/interchange/yaml_ld.py +0 -0
  93. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/llm/__init__.py +0 -0
  94. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/llm/adapters/__init__.py +0 -0
  95. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/llm/adapters/anthropic.py +0 -0
  96. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/llm/adapters/local.py +0 -0
  97. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/llm/adapters/openai_compat.py +0 -0
  98. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/llm/client.py +0 -0
  99. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/llm/errors.py +0 -0
  100. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/llm/fencing.py +0 -0
  101. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/llm/pool.py +0 -0
  102. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/llm/registry.py +0 -0
  103. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/py.typed +0 -0
  104. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/render/__init__.py +0 -0
  105. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/render/markdown.py +0 -0
  106. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/secrets.py +0 -0
  107. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/url_canonical.py +0 -0
  108. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/particles/url_safety.py +0 -0
  109. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/__init__.py +0 -0
  110. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/_client_fixtures.py +0 -0
  111. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/_upstream.py +0 -0
  112. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/MANIFEST.yaml +0 -0
  113. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/hackernews-thread-001/content.bin +0 -0
  114. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/hackernews-thread-001/manifest.yaml +0 -0
  115. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/hackernews-thread-001/snapshot.json +0 -0
  116. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/journal-entry-001/content.bin +0 -0
  117. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/journal-entry-001/manifest.yaml +0 -0
  118. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/journal-entry-001/snapshot.json +0 -0
  119. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/mastodon-thread-001/content.bin +0 -0
  120. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/mastodon-thread-001/manifest.yaml +0 -0
  121. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/mastodon-thread-001/snapshot.json +0 -0
  122. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/mcp-memory-export-001/content.bin +0 -0
  123. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/mcp-memory-export-001/manifest.yaml +0 -0
  124. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/mcp-memory-export-001/snapshot.json +0 -0
  125. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/nomisma-concept-001/content.bin +0 -0
  126. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/nomisma-concept-001/manifest.yaml +0 -0
  127. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/nomisma-concept-001/snapshot.json +0 -0
  128. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/numista-coin-001/content.bin +0 -0
  129. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/numista-coin-001/manifest.yaml +0 -0
  130. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/numista-coin-001/snapshot.json +0 -0
  131. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/numista-issuer-001/content.bin +0 -0
  132. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/numista-issuer-001/manifest.yaml +0 -0
  133. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/numista-issuer-001/snapshot.json +0 -0
  134. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/numista-listing-001/content.bin +0 -0
  135. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/numista-listing-001/manifest.yaml +0 -0
  136. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/numista-listing-001/snapshot.json +0 -0
  137. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/rdf-graph-001/content.bin +0 -0
  138. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/rdf-graph-001/manifest.yaml +0 -0
  139. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/rdf-graph-001/snapshot.json +0 -0
  140. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/reddit-thread-001/content.bin +0 -0
  141. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/reddit-thread-001/manifest.yaml +0 -0
  142. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/reddit-thread-001/snapshot.json +0 -0
  143. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/taxonomy-coins-001/content.bin +0 -0
  144. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/taxonomy-coins-001/manifest.yaml +0 -0
  145. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/taxonomy-coins-001/snapshot.json +0 -0
  146. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/trust-lens-001/content.bin +0 -0
  147. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/trust-lens-001/manifest.yaml +0 -0
  148. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/trust-lens-001/snapshot.json +0 -0
  149. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/web-article-001/content.bin +0 -0
  150. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/web-article-001/manifest.yaml +0 -0
  151. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/web-article-001/snapshot.json +0 -0
  152. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/web-article-002/content.bin +0 -0
  153. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/web-article-002/manifest.yaml +0 -0
  154. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/web-article-002/snapshot.json +0 -0
  155. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/web-essay-001/content.bin +0 -0
  156. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/web-essay-001/manifest.yaml +0 -0
  157. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/web-essay-001/snapshot.json +0 -0
  158. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/web-interview-001/content.bin +0 -0
  159. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/web-interview-001/manifest.yaml +0 -0
  160. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/web-interview-001/snapshot.json +0 -0
  161. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/wikidata-entity-001/content.bin +0 -0
  162. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/wikidata-entity-001/manifest.yaml +0 -0
  163. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conformance/fixtures/wikidata-entity-001/snapshot.json +0 -0
  164. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/conftest.py +0 -0
  165. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/fixtures/scope/normative-rule-file.md +0 -0
  166. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/fixtures/scope/normative-rule-file.recorded.json +0 -0
  167. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/fixtures/scope/rule-file-agents-md.md +0 -0
  168. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/fixtures/scope/rule-file-agents-md.recorded.json +0 -0
  169. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/test_confidence.py +0 -0
  170. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/test_config_client_sections.py +0 -0
  171. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/test_conformance_fixture_gen.py +0 -0
  172. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/test_conformance_similarity_vectors.py +0 -0
  173. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/test_conformance_validator.py +0 -0
  174. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/test_core_observer_scope.py +0 -0
  175. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/test_embeddings.py +0 -0
  176. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/test_extraction_vision.py +0 -0
  177. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/test_interchange_schema.py +0 -0
  178. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/test_llm_pool.py +0 -0
  179. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/test_schema.py +0 -0
  180. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/test_secrets.py +0 -0
  181. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/test_url_canonical.py +0 -0
  182. {linkedparticles_core-1.149.0 → linkedparticles_core-1.151.18}/tests/test_validity_extraction.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: linkedparticles-core
3
- Version: 1.149.0
3
+ Version: 1.151.18
4
4
  Summary: Store-free client library for Particles: define, validate, serialize, and extract provenance-tracked belief particles
5
5
  Project-URL: Homepage, https://linkedparticles.org
6
6
  Project-URL: Documentation, https://docs.linkedparticles.org
@@ -44,7 +44,7 @@ Requires-Dist: pyyaml>=6.0
44
44
  Requires-Dist: rdflib<8,>=7.0
45
45
  Requires-Dist: scipy>=1.13
46
46
  Requires-Dist: sentence-transformers>=2.7
47
- Requires-Dist: sqlalchemy>=2.0
47
+ Requires-Dist: sqlalchemy[asyncio]<2.2,>=2.0
48
48
  Requires-Dist: torch
49
49
  Provides-Extra: vision
50
50
  Requires-Dist: pypdfium2>=4.30; extra == 'vision'
@@ -76,7 +76,7 @@ wire format, and the extraction pipeline. Everything you need to *make* and
76
76
  > an MCP server), install
77
77
  > **[`linkedparticles`](https://pypi.org/project/linkedparticles/)** instead.
78
78
  > It depends on this package and pulls it in automatically. This one is the
79
- > substrate underneath it. Its repository,
79
+ > foundation it is built on. Its repository,
80
80
  > **[`particles-engine-py`](https://github.com/LinkedParticles/particles-engine-py)**,
81
81
  > is the reference implementation's front door: issues, discussion, and
82
82
  > documentation live there.
@@ -24,7 +24,7 @@ wire format, and the extraction pipeline. Everything you need to *make* and
24
24
  > an MCP server), install
25
25
  > **[`linkedparticles`](https://pypi.org/project/linkedparticles/)** instead.
26
26
  > It depends on this package and pulls it in automatically. This one is the
27
- > substrate underneath it. Its repository,
27
+ > foundation it is built on. Its repository,
28
28
  > **[`particles-engine-py`](https://github.com/LinkedParticles/particles-engine-py)**,
29
29
  > is the reference implementation's front door: issues, discussion, and
30
30
  > documentation live there.
@@ -12,10 +12,13 @@ trust inputs already resolved by the caller, return a ``ConflictVerdict``
12
12
  saying what should happen. It also owns the constructor that builds an
13
13
  ``INCONSISTENCY`` ``Particle`` from a conflicting pair.
14
14
 
15
+ It also owns the overrides around the verdict (:func:`decide_ladder`): the
16
+ tri-state probe, the observer precondition, and which rung 2.5 input applies.
15
17
  The *effect* half of the ladder — DB writes, trust-rank lookups, embedding
16
18
  similarity computation, the LLM contradiction-signal call — stays in
17
- ``particles/extraction/pipeline.py`` because it touches I/O. Core code must
18
- remain pure (see ``particles/core/AGENTS.md``).
19
+ ``particles/ingest/`` because it touches I/O: ``conflict_plan`` maps an outcome
20
+ to writes, and ``pipeline`` gathers the inputs and applies the plan. Core code
21
+ must remain pure (see ``particles/core/AGENTS.md``).
19
22
 
20
23
  Ladder (normative, applied in order — §6.4, rung for rung):
21
24
 
@@ -83,9 +86,11 @@ Two extra verdicts are emitted by the pre-ladder gate the caller may apply:
83
86
 
84
87
  from __future__ import annotations
85
88
 
89
+ from dataclasses import dataclass
86
90
  from enum import StrEnum
87
91
  from typing import Any
88
92
 
93
+ from particles.core.observer_scope import PairPrecondition
89
94
  from particles.core.schema import (
90
95
  SCHEMA_VERSION,
91
96
  Confidence,
@@ -309,6 +314,165 @@ def resolve_conflict(
309
314
  return ConflictVerdict.INCONSISTENT
310
315
 
311
316
 
317
+ @dataclass(frozen=True)
318
+ class RungInputs:
319
+ """The gathered §6.4 rung inputs for one pair; the defaults are "no input".
320
+
321
+ The caller resolves these only when :func:`needs_rung_inputs` says the
322
+ ladder can consult them (a confirmed or fail-closed signal on a pair the
323
+ observer precondition does not decline).
324
+ """
325
+
326
+ new_supersedes_existing: bool = False
327
+ """Rung 1.5: ``new``'s document (transitively) supersedes ``existing``'s."""
328
+ existing_supersedes_new: bool = False
329
+ """Rung 1.5 mirror."""
330
+ trust_score_new: float | None = None
331
+ """Rung 2: the candidate's pre-resolved trust score."""
332
+ trust_score_existing: float | None = None
333
+ """Rung 2: the existing particle's pre-resolved trust score."""
334
+ update_order: int | None = None
335
+ """Rung 2.5: see :func:`resolve_conflict`'s ``update_order``."""
336
+
337
+
338
+ @dataclass(frozen=True)
339
+ class LadderOutcome:
340
+ """What the §6.6 ladder decided for one pair, overrides included."""
341
+
342
+ verdict: ConflictVerdict | None
343
+ """The verdict; ``None`` when the observer precondition declined the pair:
344
+ the candidate is written ``ACTIVE`` beside the existing claim."""
345
+ record_divergence: bool
346
+ """Declined and the signal is set: the pair is recorded as a divergence."""
347
+
348
+
349
+ class UpdateOrderSource(StrEnum):
350
+ """Which rung 2.5 input the caller gathers for a pair."""
351
+
352
+ UPDATE = "update"
353
+ """Extraction's same-lineage update order."""
354
+ OWN_ASSERTION = "own_assertion"
355
+ """The assertion pathway's own-assertion order."""
356
+
357
+
358
+ def ladder_signal(probe: bool | None, *, fail_closed: bool) -> bool:
359
+ """The contradiction signal the ladder runs on, from a tri-state probe.
360
+
361
+ ``True``/``False`` is a verdict. ``None`` means the probe could not
362
+ complete: extraction stays fail-open (no signal), and the assertion pathway
363
+ fails closed (a signal, which :func:`forces_inconsistent` then overrides to
364
+ ``INCONSISTENT``).
365
+ """
366
+ if probe is None:
367
+ return fail_closed
368
+ return probe
369
+
370
+
371
+ def forces_inconsistent(
372
+ probe: bool | None, *, fail_closed: bool, precondition: PairPrecondition
373
+ ) -> bool:
374
+ """Whether the verdict is ``INCONSISTENT`` whatever the rungs would say.
375
+
376
+ Two overrides: an incomplete probe under ``fail_closed``, and
377
+ a project's candidate contesting a global claim with a signal (
378
+ ``REVIEW``). A ``DECLINE`` outranks both; :func:`decide_ladder` checks it
379
+ first.
380
+ """
381
+ if probe is None and fail_closed:
382
+ return True
383
+ return precondition is PairPrecondition.REVIEW and ladder_signal(probe, fail_closed=fail_closed)
384
+
385
+
386
+ def needs_rung_inputs(
387
+ probe: bool | None, *, fail_closed: bool, precondition: PairPrecondition
388
+ ) -> bool:
389
+ """Whether the caller should gather :class:`RungInputs` for the pair.
390
+
391
+ Only a set signal reaches the rungs, and a declined pair reaches none. A
392
+ forced ``INCONSISTENT`` still gathers: the domain read alongside the trust
393
+ scores is the INCONSISTENCY record's ``domain_hint``.
394
+ """
395
+ if precondition is PairPrecondition.DECLINE:
396
+ return False
397
+ return ladder_signal(probe, fail_closed=fail_closed)
398
+
399
+
400
+ def update_order_source(
401
+ *,
402
+ allow_update: bool,
403
+ allow_own_assertion: bool,
404
+ forced: bool,
405
+ ) -> UpdateOrderSource | None:
406
+ """Which rung 2.5 input applies, if any.
407
+
408
+ ``allow_update`` is extraction's door with the config switch already folded
409
+ in; ``allow_own_assertion`` the assertion pathway's narrower one.
410
+ A forced ``INCONSISTENT`` consults neither.
411
+ """
412
+ if forced:
413
+ return None
414
+ if allow_update:
415
+ return UpdateOrderSource.UPDATE
416
+ if allow_own_assertion:
417
+ return UpdateOrderSource.OWN_ASSERTION
418
+ return None
419
+
420
+
421
+ def effective_single_trust_order(override: bool | None, store_mode: str) -> bool:
422
+ """The trust regime the ladder runs under.
423
+
424
+ A caller's explicit ``override`` wins; otherwise the store mode decides.
425
+ """
426
+ if override is not None:
427
+ return override
428
+ return store_mode == "single"
429
+
430
+
431
+ def decide_ladder(
432
+ existing: Particle,
433
+ new: Particle,
434
+ *,
435
+ probe: bool | None,
436
+ fail_closed: bool,
437
+ precondition: PairPrecondition,
438
+ rung_inputs: RungInputs | None,
439
+ single_trust_order: bool,
440
+ trust_differential_threshold: float,
441
+ ) -> LadderOutcome:
442
+ """The §6.6 ladder for one pair, with every override applied in order.
443
+
444
+ Pure: the caller gathers the probe, the precondition and (when
445
+ :func:`needs_rung_inputs`) the rung inputs, then applies the outcome. The
446
+ override order is:
447
+
448
+ 1. ``DECLINE``: no rung runs, whatever the probe said. The
449
+ divergence is recorded when the signal is set, a fail-closed
450
+ incomplete probe included.
451
+ 2. a forced ``INCONSISTENT`` (:func:`forces_inconsistent`): the rungs are
452
+ skipped.
453
+ 3. otherwise :func:`resolve_conflict` on the signal and the rung inputs.
454
+ """
455
+ signal = ladder_signal(probe, fail_closed=fail_closed)
456
+ if precondition is PairPrecondition.DECLINE:
457
+ return LadderOutcome(verdict=None, record_divergence=signal)
458
+ if forces_inconsistent(probe, fail_closed=fail_closed, precondition=precondition):
459
+ return LadderOutcome(verdict=ConflictVerdict.INCONSISTENT, record_divergence=False)
460
+ inputs = rung_inputs if rung_inputs is not None else RungInputs()
461
+ verdict = resolve_conflict(
462
+ existing,
463
+ new,
464
+ has_contradiction_signal=signal,
465
+ new_supersedes_existing=inputs.new_supersedes_existing,
466
+ existing_supersedes_new=inputs.existing_supersedes_new,
467
+ trust_score_existing=inputs.trust_score_existing,
468
+ trust_score_new=inputs.trust_score_new,
469
+ trust_differential_threshold=trust_differential_threshold,
470
+ single_trust_order=single_trust_order,
471
+ update_order=inputs.update_order,
472
+ )
473
+ return LadderOutcome(verdict=verdict, record_divergence=False)
474
+
475
+
312
476
  # the ``properties`` marker on an INCONSISTENCY record whose
313
477
  # "Particle A" is a judgment-retired twin rather than a live claim. Its value
314
478
  # is the retired twin's ``status_reason``. Review reads it to know that
@@ -0,0 +1,310 @@
1
+ # SPDX-FileCopyrightText: 2026 The Particles authors
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ """Pure INCONSISTENCY-resolution decisions shared by Review and the trust cascade.
6
+
7
+ An INCONSISTENCY wrapper names two conflicting claims, A and B. Two operations
8
+ close it: a human review (§9.6, :mod:`particles.operations.review`) and the
9
+ source trust cascade (Extension B, :mod:`particles.operations.cascade`). Both
10
+ decide the same things — which claim loses, what demoting it means given its
11
+ current status, whether a quarantined claim is promoted, and which corpus
12
+ entry a trust statement is keyed on — and those decisions are pure functions
13
+ of the wrapper, the two claims and (for the cascade) their trust ranks.
14
+
15
+ They live here so each rule exists once and is testable without a database
16
+ (D2). The operations keep the I/O half: they gather the wrapper and
17
+ the claims, call these functions, and apply the plan in their existing write
18
+ order. Same split as :mod:`particles.core.cascade_gate`.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from dataclasses import dataclass
24
+ from enum import Enum
25
+
26
+ from particles.core.conflict_resolution import RETIRED_VALUE_KEY
27
+ from particles.core.schema import Particle, ProvenanceRefType, ResolutionAction
28
+ from particles.core.status import Status, StatusReason
29
+
30
+ _TERMINAL = (Status.RETRACTED, Status.SUPERSEDED)
31
+
32
+
33
+ class Demotion(Enum):
34
+ """What demoting a conflict's losing claim takes, given its current status."""
35
+
36
+ NONE = "none"
37
+ """Nothing to write: the claim is missing (a pre-ADR-0117 dangling ref),
38
+ already terminal, or stale for a reason other than a pending
39
+ conflict."""
40
+
41
+ REASON_FLIP = "reason_flip"
42
+ """A quarantined claim: ``CONFLICT_PENDING → CONFLICT_RESOLVED``
43
+ in place, with no status transition."""
44
+
45
+ TRANSITION = "transition"
46
+ """A live claim: transition to ``PROVENANCE_STALE`` / ``CONFLICT_RESOLVED``."""
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class AleatoryMark:
51
+ """One BOTH_VALID claim to keep queryable with ``ALEATORY`` nature."""
52
+
53
+ particle: Particle
54
+ mint: bool
55
+ """True for a quarantined claim, which is promoted to a new ACTIVE
56
+ particle carrying the nature; False to update the nature in place."""
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class TrustJudgment:
61
+ """The SourceTrustStatement a PREFER review writes, before it is minted."""
62
+
63
+ source_entry_id: str
64
+ """Corpus entry the statement is keyed on."""
65
+ preferred_id: str
66
+ """The preferred claim as stored. When the plan promotes it, the operation
67
+ names the minted particle instead."""
68
+ demoted_id: str | None
69
+ """The other wrapper ref, which may dangle on a pre-ADR-0117 wrapper."""
70
+
71
+
72
+ @dataclass(frozen=True)
73
+ class ResolutionPlan:
74
+ """Every write a review resolution makes, decided up front.
75
+
76
+ The operation applies the fields in declaration order, then closes (or,
77
+ for DEFER, re-sets) the wrapper, and only then runs the trust cascade
78
+ when ``trust`` is set. The wrapper closes first so the cascade never
79
+ re-processes the resolution just made.
80
+ """
81
+
82
+ demote: tuple[Particle, Demotion] | None
83
+ promote: Particle | None
84
+ aleatory: tuple[AleatoryMark, ...]
85
+ trust: TrustJudgment | None
86
+ """None: no trust statement is written, and no cascade runs."""
87
+ close_wrapper: bool
88
+ """False only for DEFER, the one action that leaves the wrapper open."""
89
+
90
+
91
+ @dataclass(frozen=True)
92
+ class CascadeVerdict:
93
+ """A trust-cascade resolution of one wrapper."""
94
+
95
+ winner: Particle
96
+ loser: Particle
97
+ loser_demotion: Demotion
98
+ promote_winner: bool
99
+ """True when the winner is quarantined: it is promoted to a new ACTIVE
100
+ particle, as a PREFER_B review would."""
101
+
102
+
103
+ def is_quarantined(particle: Particle) -> bool:
104
+ """True for a §6.6 quarantined conflict loser."""
105
+ return (
106
+ particle.status is Status.PROVENANCE_STALE
107
+ and particle.status_reason is StatusReason.CONFLICT_PENDING
108
+ )
109
+
110
+
111
+ def conflict_pair_ids(inconsistency: Particle) -> tuple[str | None, str | None]:
112
+ """The ids of claims A and B named by an INCONSISTENCY wrapper.
113
+
114
+ By convention the first PARTICLE provenance ref is A and the second is B;
115
+ the referenced particle id travels in ``corpus_entry_id``. A missing ref
116
+ reads as None.
117
+ """
118
+ refs = [
119
+ r.corpus_entry_id for r in inconsistency.provenance if r.type is ProvenanceRefType.PARTICLE
120
+ ]
121
+ return (
122
+ refs[0] if len(refs) > 0 else None,
123
+ refs[1] if len(refs) > 1 else None,
124
+ )
125
+
126
+
127
+ def is_retired_value(inconsistency: Particle) -> bool:
128
+ """Whether the wrapper records a retired value rather than a source conflict.
129
+
130
+ A retired-value record pairs a judgment-retired twin (A) with a held
131
+ re-assertion (B). Resolving it rules on the earlier judgment, not on a
132
+ source, so neither review nor the cascade writes a trust verdict for it.
133
+ """
134
+ return bool(inconsistency.properties and inconsistency.properties.get(RETIRED_VALUE_KEY))
135
+
136
+
137
+ def decide_demotion(loser: Particle | None) -> Demotion:
138
+ """What demoting a conflict's losing claim takes.
139
+
140
+ A missing loser (a pre-ADR-0117 wrapper's dangling ref) and a terminal one
141
+ (a judgment-retired twin) need nothing: there is no legal
142
+ transition out of a terminal state. A stale loser is already demoted; only
143
+ a quarantined one has its reason flipped.
144
+ """
145
+ if loser is None or loser.status in _TERMINAL:
146
+ return Demotion.NONE
147
+ if loser.status is Status.PROVENANCE_STALE:
148
+ if loser.status_reason is StatusReason.CONFLICT_PENDING:
149
+ return Demotion.REASON_FLIP
150
+ return Demotion.NONE
151
+ return Demotion.TRANSITION
152
+
153
+
154
+ def _source_entry_id(particle: Particle) -> str | None:
155
+ """The corpus entry of the claim's first SOURCE provenance ref."""
156
+ ref = next((r for r in particle.provenance if r.type is ProvenanceRefType.SOURCE), None)
157
+ return ref.corpus_entry_id if ref is not None else None
158
+
159
+
160
+ def trust_statement_source(preferred: Particle | None) -> str | None:
161
+ """The corpus-entry id a PREFER judgment's trust statement is keyed on.
162
+
163
+ Every consumer of a SourceTrustStatement matches its ``source_ref``
164
+ against a corpus-entry id: the §6.4 layered cascade and the reviewer
165
+ confirmation gate. The statement is therefore keyed on
166
+ the corpus entry of the preferred claim's SOURCE provenance, never on the
167
+ particle id, which no consumer could look up.
168
+
169
+ Returns None, meaning no statement is written, when the preferred claim is
170
+ unknown (a dangling ref), is not ACTIVE (a retired claim is no source
171
+ recommendation), or has no SOURCE provenance (an agent-asserted or
172
+ derived claim names no corpus entry).
173
+ """
174
+ if preferred is None or preferred.status is not Status.ACTIVE:
175
+ return None
176
+ return _source_entry_id(preferred)
177
+
178
+
179
+ def decide_resolution(
180
+ action: ResolutionAction,
181
+ inconsistency: Particle,
182
+ particle_a: Particle | None,
183
+ particle_b: Particle | None,
184
+ ) -> ResolutionPlan:
185
+ """Decide every write a review resolution makes (§9.6).
186
+
187
+ Args:
188
+ action: The reviewer's resolution.
189
+ inconsistency: The INCONSISTENCY wrapper.
190
+ particle_a: Claim A as stored, or None when its ref dangles.
191
+ particle_b: Claim B as stored, or None when its ref dangles.
192
+
193
+ Returns:
194
+ The plan the operation applies.
195
+ """
196
+ a_id, b_id = conflict_pair_ids(inconsistency)
197
+ retired_value = is_retired_value(inconsistency)
198
+
199
+ match action:
200
+ case ResolutionAction.PREFER_A:
201
+ trust_source = trust_statement_source(particle_a)
202
+ trust = (
203
+ TrustJudgment(trust_source, particle_a.id, b_id)
204
+ if trust_source is not None and particle_a is not None and not retired_value
205
+ else None
206
+ )
207
+ return ResolutionPlan(
208
+ demote=_demote(particle_b),
209
+ promote=None,
210
+ aleatory=(),
211
+ trust=trust,
212
+ close_wrapper=True,
213
+ )
214
+ case ResolutionAction.PREFER_B:
215
+ # A quarantined B is minted as a new ACTIVE particle carrying its
216
+ # provenance verbatim, so the statement is keyed on B's source as
217
+ # if B were ACTIVE; any other B is judged as stored.
218
+ promote = particle_b if particle_b is not None and is_quarantined(particle_b) else None
219
+ trust_source = (
220
+ _source_entry_id(promote)
221
+ if promote is not None
222
+ else trust_statement_source(particle_b)
223
+ )
224
+ trust = (
225
+ TrustJudgment(trust_source, particle_b.id, a_id)
226
+ if trust_source is not None and particle_b is not None and not retired_value
227
+ else None
228
+ )
229
+ return ResolutionPlan(
230
+ demote=_demote(particle_a),
231
+ promote=promote,
232
+ aleatory=(),
233
+ trust=trust,
234
+ close_wrapper=True,
235
+ )
236
+ case ResolutionAction.BOTH_VALID:
237
+ marks = tuple(
238
+ AleatoryMark(particle=p, mint=is_quarantined(p))
239
+ for p in (particle_a, particle_b)
240
+ if p is not None
241
+ )
242
+ return ResolutionPlan(
243
+ demote=None, promote=None, aleatory=marks, trust=None, close_wrapper=True
244
+ )
245
+ case ResolutionAction.DEFER:
246
+ return ResolutionPlan(
247
+ demote=None, promote=None, aleatory=(), trust=None, close_wrapper=False
248
+ )
249
+
250
+
251
+ def _demote(loser: Particle | None) -> tuple[Particle, Demotion] | None:
252
+ return (loser, decide_demotion(loser)) if loser is not None else None
253
+
254
+
255
+ def cascade_pair(
256
+ inconsistency: Particle, particle_a: Particle | None, particle_b: Particle | None
257
+ ) -> tuple[Particle, Particle] | None:
258
+ """The structural half of the cascade decision: is this wrapper rankable?
259
+
260
+ The trust cascade leaves a wrapper for a person when either claim is
261
+ missing (a pre-ADR-0117 dangling ref), when it records a retired value, or
262
+ when A has since left the surface by a terminal transition: source trust
263
+ cannot answer a question about an earlier judgment.
264
+
265
+ The operation calls this before looking up trust ranks, so a skipped
266
+ wrapper costs no rank lookups. :func:`decide_cascade` applies it again,
267
+ so it is a complete decision on its own.
268
+
269
+ Returns:
270
+ ``(A, B)`` to rank, or None when the wrapper is left open.
271
+ """
272
+ if particle_a is None or particle_b is None:
273
+ return None
274
+ if is_retired_value(inconsistency) or particle_a.status in _TERMINAL:
275
+ return None
276
+ return particle_a, particle_b
277
+
278
+
279
+ def decide_cascade(
280
+ inconsistency: Particle,
281
+ particle_a: Particle | None,
282
+ particle_b: Particle | None,
283
+ rank_a: float | None,
284
+ rank_b: float | None,
285
+ *,
286
+ differential_threshold: float,
287
+ ) -> CascadeVerdict | None:
288
+ """Decide whether the trust cascade resolves a wrapper, and for whom.
289
+
290
+ The claim whose source ranks higher wins, provided both ranks are known
291
+ and differ by at least ``differential_threshold``. On an exact tie that
292
+ clears a zero threshold, B wins.
293
+
294
+ Returns:
295
+ The verdict, or None when the wrapper stays open for manual review.
296
+ """
297
+ pair = cascade_pair(inconsistency, particle_a, particle_b)
298
+ if pair is None or rank_a is None or rank_b is None:
299
+ return None
300
+ diff = rank_a - rank_b
301
+ if abs(diff) < differential_threshold:
302
+ return None
303
+ a, b = pair
304
+ winner, loser = (a, b) if diff > 0 else (b, a)
305
+ return CascadeVerdict(
306
+ winner=winner,
307
+ loser=loser,
308
+ loser_demotion=decide_demotion(loser),
309
+ promote_winner=is_quarantined(winner),
310
+ )
@@ -21,6 +21,10 @@ with cross-lingual detection and the ``language`` field.
21
21
 
22
22
  from __future__ import annotations
23
23
 
24
+ from collections.abc import Iterable
25
+
26
+ from particles.core.schema import ParticleRelation
27
+
24
28
 
25
29
  def effective_equivalence(link_confidence: float, *, observer_trust: float | None = None) -> float:
26
30
  """Observer-relative claim-equivalence strength in ``[0, 1]`` (§6.10).
@@ -38,3 +42,45 @@ def effective_equivalence(link_confidence: float, *, observer_trust: float | Non
38
42
  if observer_trust is None:
39
43
  return link_confidence
40
44
  return max(0.0, min(1.0, link_confidence * observer_trust))
45
+
46
+
47
+ def co_evidential_components(
48
+ edges: Iterable[ParticleRelation], min_confidence: float
49
+ ) -> dict[str, frozenset[str]]:
50
+ """Group ids into CO_EVIDENTIAL components from the whole edge list at once.
51
+
52
+ The in-memory equivalent of running ``get_co_evidential_group`` per particle,
53
+ honouring the same ``effective_equivalence >= min_confidence`` gate, so a
54
+ caller reads the relation table once and decides "already linked" without a
55
+ session (D2). Ids with no qualifying edge are simply
56
+ absent: their component is the singleton the caller supplies.
57
+
58
+ Args:
59
+ edges: CO_EVIDENTIAL edges, e.g. ``get_all_relations(session, CO_EVIDENTIAL)``.
60
+ min_confidence: Edges whose effective equivalence falls below this are
61
+ not traversed.
62
+
63
+ Returns:
64
+ Every id that sits on a qualifying edge, mapped to its whole component
65
+ (itself included).
66
+ """
67
+ parent: dict[str, str] = {}
68
+
69
+ def find(x: str) -> str:
70
+ parent.setdefault(x, x)
71
+ while parent[x] != x:
72
+ parent[x] = parent[parent[x]]
73
+ x = parent[x]
74
+ return x
75
+
76
+ for e in edges:
77
+ if effective_equivalence(e.confidence) < min_confidence:
78
+ continue
79
+ a, b = find(e.particle_a), find(e.particle_b)
80
+ if a != b:
81
+ parent[a] = b
82
+
83
+ groups: dict[str, set[str]] = {}
84
+ for node in list(parent):
85
+ groups.setdefault(find(node), set()).add(node)
86
+ return {node: frozenset(members) for members in groups.values() for node in members}
@@ -0,0 +1,38 @@
1
+ # SPDX-FileCopyrightText: 2026 The Particles authors
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ """The shape of an appended provenance observation (D1).
6
+
7
+ Provenance is append-only: a re-observation of a claim is a new ref, never an
8
+ edit of an old one. This module holds the pure rule for what that ref looks
9
+ like, so it is testable without a store; the write stays with
10
+ ``particle_store.append_provenance_ref``, the one provenance writer.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from particles.core.schema import Particle, ProvenanceRef, ProvenanceRefType
16
+
17
+
18
+ def reobservation_ref(particle: Particle, entry_id: str, snapshot_id: str) -> ProvenanceRef:
19
+ """Return the ref recording that ``snapshot_id`` re-observed ``particle``.
20
+
21
+ The ref copies the particle's latest SOURCE ref to ``entry_id`` (its
22
+ location and chunk hash are the carried chunk's) and names the
23
+ re-observing snapshot. A particle with no SOURCE ref to the entry gets a
24
+ bare SOURCE ref naming the entry and snapshot.
25
+ """
26
+ prior = next(
27
+ (
28
+ r
29
+ for r in reversed(particle.provenance)
30
+ if r.type is ProvenanceRefType.SOURCE and r.corpus_entry_id == entry_id
31
+ ),
32
+ None,
33
+ )
34
+ if prior is not None:
35
+ return prior.model_copy(update={"snapshot_id": snapshot_id})
36
+ return ProvenanceRef(
37
+ type=ProvenanceRefType.SOURCE, corpus_entry_id=entry_id, snapshot_id=snapshot_id
38
+ )
@@ -33,7 +33,7 @@ size — narrative grouping (one label, one sequence) is a whole-document concer
33
33
  Over-length entries (> ``extraction.html_chunk_size``) are extracted in multiple
34
34
  carry-forward passes and their per-chunk NARRATIVE fragments are
35
35
  merged into one whole-entry NARRATIVE by the Engine-side post-pass
36
- (:func:`particles.ingest.narrative_merge.collapse_chunk_narratives`).
36
+ (:func:`particles.ingest.narrative_merge.collapse_narratives`).
37
37
  """
38
38
 
39
39
  from __future__ import annotations
@@ -283,7 +283,7 @@ class JournalExtractor:
283
283
  (2). Each cache-miss chunk yields its claims (chunk-local
284
284
  ``narrative_index``) plus one per-chunk NARRATIVE candidate; the Engine
285
285
  post-pass
286
- (:func:`particles.ingest.narrative_merge.collapse_chunk_narratives`)
286
+ (:func:`particles.ingest.narrative_merge.collapse_narratives`)
287
287
  collapses those fragments into one whole-entry NARRATIVE with a global
288
288
  ``SEQUENCE_IN`` order. Chunks beyond ``max_llm_calls_per_source`` emit
289
289
  the shared ``CHUNK_TRUNCATION`` note — the new, much higher truncation
@@ -19,7 +19,7 @@
19
19
  # version and advance together.
20
20
  [project]
21
21
  name = "linkedparticles-core"
22
- version = "1.149.0"
22
+ version = "1.151.18"
23
23
  description = "Store-free client library for Particles: define, validate, serialize, and extract provenance-tracked belief particles"
24
24
  readme = "README.md"
25
25
  requires-python = ">=3.11"
@@ -80,8 +80,10 @@ dependencies = [
80
80
  # The extraction layer types its session parameters against SQLAlchemy's
81
81
  # async session; the import is executed at module load even though no
82
82
  # Client module opens a connection. Declared so a store-free install still
83
- # imports.
84
- "sqlalchemy>=2.0",
83
+ # imports, and with `[asyncio]` because that import needs greenlet, which
84
+ # SQLAlchemy stopped installing by default in 2.1. Bounded below the next
85
+ # minor: 2.1 changed ORM and typing behaviour this code depends on.
86
+ "sqlalchemy[asyncio]>=2.0,<2.2",
85
87
  # JSON-LD / RDF / SHACL parsing stack — these handle attacker-influenced
86
88
  # input (interchange import, conformance validation), so they carry tested
87
89
  # next-breaking upper bounds in addition to the floor: a future breaking