argus-cortex 0.4.3__tar.gz → 0.4.4__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 (47) hide show
  1. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/CHANGELOG.md +16 -1
  2. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/PKG-INFO +1 -1
  3. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/_version.py +2 -2
  4. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/moderation/singguard.py +3 -1
  5. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/store/lineage.py +15 -5
  6. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/tests/test_moderation.py +18 -0
  7. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/tests/test_store.py +5 -3
  8. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/tests/test_store_pg_integration.py +14 -0
  9. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/.copier-answers.yml +0 -0
  10. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/.env.example +0 -0
  11. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/.github/workflows/ci.yml +0 -0
  12. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/.github/workflows/release.yml +0 -0
  13. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/.gitignore +0 -0
  14. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/CLAUDE.md +0 -0
  15. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/LICENSE +0 -0
  16. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/Makefile +0 -0
  17. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/README.md +0 -0
  18. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/pyproject.toml +0 -0
  19. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/__init__.py +0 -0
  20. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/backends.py +0 -0
  21. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/gpu.py +0 -0
  22. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/moderation/__init__.py +0 -0
  23. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/moderation/_http.py +0 -0
  24. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/moderation/nudenet.py +0 -0
  25. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/moderation/policies.py +0 -0
  26. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/py.typed +0 -0
  27. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/server.py +0 -0
  28. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/store/__init__.py +0 -0
  29. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/store/blob.py +0 -0
  30. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/store/config.py +0 -0
  31. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/store/errors.py +0 -0
  32. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/store/models.py +0 -0
  33. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/store/schema.py +0 -0
  34. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/store/vector.py +0 -0
  35. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/taxonomy.py +0 -0
  36. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/wire.py +0 -0
  37. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/src/argus_cortex/xmp.py +0 -0
  38. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/tests/test_backends.py +0 -0
  39. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/tests/test_blob.py +0 -0
  40. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/tests/test_gpu.py +0 -0
  41. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/tests/test_policies.py +0 -0
  42. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/tests/test_server.py +0 -0
  43. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/tests/test_smoke.py +0 -0
  44. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/tests/test_taxonomy.py +0 -0
  45. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/tests/test_vector.py +0 -0
  46. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/tests/test_wire.py +0 -0
  47. {argus_cortex-0.4.3 → argus_cortex-0.4.4}/tests/test_xmp.py +0 -0
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.4] - 2026-10-31
11
+
12
+ ### Fixed
13
+ - **`SingGuardClient` keeps a longer writeup.** `to_result` clipped `explanation` at 4000
14
+ characters, which cut cascade/slow reasoning mid-sentence. The cap is now 16384, enough
15
+ for a 2048-token slow writeup plus a cascade preface.
16
+ - **Re-recording an asset no longer erases what another stage recorded on it.** `record_asset`'s
17
+ upsert on `sha256` replaced `immich_id` and `metadata` outright, so argus-lens recording a caption
18
+ for an image argus-quarry had already recorded (licence, author, source) wiped that provenance,
19
+ and a call without an `immich_id` cleared the stored one. `metadata` now merges key by key
20
+ (`jsonb ||`: the new call's keys win, others are kept) and a `NULL` `immich_id` keeps the stored
21
+ one. `uri` is still last-write-wins: it is the asset's latest location. A caller that needs to
22
+ remove a metadata key can no longer do it through `record_asset`.
23
+
10
24
  ## [0.4.3] - 2026-10-30
11
25
 
12
26
  ### Added
@@ -164,7 +178,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
164
178
  (`LocalBackend`, `RemoteBackend`, `RemoteProvider`, `with_retries`
165
179
  retrying only transient errors, `resolve_device`).
166
180
 
167
- [Unreleased]: https://github.com/smk762/argus-cortex/compare/v0.4.3...HEAD
181
+ [Unreleased]: https://github.com/smk762/argus-cortex/compare/v0.4.4...HEAD
182
+ [0.4.4]: https://github.com/smk762/argus-cortex/compare/v0.4.3...v0.4.4
168
183
  [0.4.3]: https://github.com/smk762/argus-cortex/compare/v0.4.2...v0.4.3
169
184
  [0.4.2]: https://github.com/smk762/argus-cortex/compare/v0.4.1...v0.4.2
170
185
  [0.4.1]: https://github.com/smk762/argus-cortex/compare/v0.4.0...v0.4.1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: argus-cortex
3
- Version: 0.4.3
3
+ Version: 0.4.4
4
4
  Summary: Shared foundation for the Argus suite: taxonomy, versioned wire-schema tooling, and the local/remote AI backend contract
5
5
  Project-URL: Repository, https://github.com/smk762/argus-cortex
6
6
  Author: smk762
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.4.3'
22
- __version_tuple__ = version_tuple = (0, 4, 3)
21
+ __version__ = version = '0.4.4'
22
+ __version_tuple__ = version_tuple = (0, 4, 4)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -47,7 +47,9 @@ MAX_EDGE = 1536
47
47
  MAX_BYTES = 4 * 1024 * 1024
48
48
  _MIN_EDGE = 256 # below this, give up rather than send SingGuard a postage stamp
49
49
  _NEEDS_PILLOW = "downscaling images needs: pip install argus-cortex[images] (or pass downscale=False)"
50
- _EXPLANATION_MAX = 4000
50
+ # Fits a 2048-token slow writeup plus a short cascade preface. 4000
51
+ # cut recorded reasoning mid-sentence on cascade scans.
52
+ _EXPLANATION_MAX = 16384
51
53
  _FIELD_MAX = 200
52
54
 
53
55
 
@@ -70,7 +70,13 @@ class LineageStore(Protocol):
70
70
  ...
71
71
 
72
72
  def record_asset(self, asset: SourceAsset) -> str | None:
73
- """Insert (or reuse, by ``sha256``) a source asset; return its id."""
73
+ """Insert (or reuse, by ``sha256``) a source asset; return its id.
74
+
75
+ On reuse, ``uri`` becomes this call's; ``metadata`` merges into the
76
+ stored bag (this call's keys win); a ``None`` ``immich_id`` keeps the
77
+ stored one. So stages recording the same bytes add to the row rather
78
+ than erase each other's fields.
79
+ """
74
80
  ...
75
81
 
76
82
  def record_caption(self, asset_id: str, caption: Caption) -> str | None:
@@ -302,8 +308,12 @@ class PostgresLineageStore:
302
308
 
303
309
  def record_asset(self, asset: SourceAsset) -> str | None:
304
310
  # De-dupe on content hash: re-ingesting identical bytes returns the
305
- # existing row (DO UPDATE ensures RETURNING fires even on conflict) and
306
- # refreshes uri/immich_id/metadata to the latest call (last-write-wins).
311
+ # existing row (DO UPDATE ensures RETURNING fires even on conflict).
312
+ # Several stages record the same asset (quarry with provenance, lens
313
+ # with a caption), so a later call must not erase what an earlier one
314
+ # knew: uri is the latest location (last-write-wins), immich_id keeps
315
+ # the stored id when this call has none, and metadata merges key by
316
+ # key (jsonb ||: this call's keys win, the others are kept).
307
317
  # NOTE: dedup only works when sha256 is set — NULLs are distinct under the
308
318
  # UNIQUE index, so a hash-less asset inserts a fresh row every call.
309
319
  return self._insert(
@@ -312,8 +322,8 @@ class PostgresLineageStore:
312
322
  VALUES (%s, %s, %s, %s::jsonb)
313
323
  ON CONFLICT (sha256) DO UPDATE
314
324
  SET uri = EXCLUDED.uri,
315
- immich_id = EXCLUDED.immich_id,
316
- metadata = EXCLUDED.metadata
325
+ immich_id = COALESCE(EXCLUDED.immich_id, source_asset.immich_id),
326
+ metadata = source_asset.metadata || EXCLUDED.metadata
317
327
  RETURNING id
318
328
  """,
319
329
  (asset.uri, asset.sha256, asset.immich_id, self._json(asset.metadata)),
@@ -253,6 +253,24 @@ def test_singguard_config() -> None:
253
253
  SingGuardClient("gopher://g")
254
254
 
255
255
 
256
+ def test_to_result_keeps_a_long_explanation() -> None:
257
+ writeup = "Cascade: fast flagged this; the slow pass is the verdict.\n\n" + ("A posed adult. " * 400)
258
+ writeup = writeup.strip()
259
+ assert 4000 < len(writeup) < singguard._EXPLANATION_MAX
260
+ result = singguard.to_result(
261
+ {"safe": True, "verdict": "safe", "rule": "Safe", "explanation": writeup, "categories": []}
262
+ )
263
+ assert result.explanation == writeup
264
+
265
+
266
+ def test_to_result_clips_explanation_at_max() -> None:
267
+ exact = "y" * singguard._EXPLANATION_MAX
268
+ over = exact + "z"
269
+ payload = {"safe": True, "verdict": "safe", "rule": "Safe", "categories": []}
270
+ assert singguard.to_result({**payload, "explanation": exact}).explanation == exact
271
+ assert singguard.to_result({**payload, "explanation": over}).explanation == exact
272
+
273
+
256
274
  def test_singguard_check(singguard_server) -> None:
257
275
  fake, url = singguard_server
258
276
  client = SingGuardClient(url, timeout=5, thinking="fast-slow", policy="### A. Minors")
@@ -206,9 +206,11 @@ def test_record_asset_returns_id_and_upserts_on_sha() -> None:
206
206
  sql, params = conn.executed[-1]
207
207
  assert "INSERT INTO source_asset" in sql
208
208
  assert "ON CONFLICT (sha256)" in sql
209
- # on a dedup hit, uri/immich_id/metadata are all refreshed (not just uri)
210
- assert "immich_id = EXCLUDED.immich_id" in sql
211
- assert "metadata = EXCLUDED.metadata" in sql
209
+ # on a dedup hit uri is refreshed, but a call without an immich_id or a
210
+ # metadata key keeps the stored one (another stage may have recorded it)
211
+ assert "uri = EXCLUDED.uri" in sql
212
+ assert "immich_id = COALESCE(EXCLUDED.immich_id, source_asset.immich_id)" in sql
213
+ assert "metadata = source_asset.metadata || EXCLUDED.metadata" in sql
212
214
  # metadata is serialised to a JSON string for the ::jsonb cast
213
215
  assert params == ("immich://1", "abc", None, '{"k": "v"}')
214
216
 
@@ -81,6 +81,20 @@ def test_transaction_rolls_back_on_error(store: PostgresLineageStore) -> None:
81
81
  assert reused is not None
82
82
 
83
83
 
84
+ def test_reupsert_merges_metadata_and_keeps_immich_id(store: PostgresLineageStore) -> None:
85
+ # quarry records provenance first; lens later records the same bytes with
86
+ # none: the provenance and immich id survive, the uri moves, shared keys update.
87
+ sha = uuid.uuid4().hex
88
+ first = store.record_asset(
89
+ SourceAsset(uri="quarry://a", sha256=sha, immich_id="im-1", metadata={"license": "cc0", "seen": 1})
90
+ )
91
+ again = store.record_asset(SourceAsset(uri="/data/images/a.png", sha256=sha, metadata={"seen": 2}))
92
+ assert again == first
93
+ with store._acquire() as conn:
94
+ row = conn.execute("SELECT uri, immich_id, metadata FROM source_asset WHERE id = %s", (first,)).fetchone()
95
+ assert row == ("/data/images/a.png", "im-1", {"license": "cc0", "seen": 2})
96
+
97
+
84
98
  def test_concurrent_writes_are_isolated(store: PostgresLineageStore) -> None:
85
99
  # Many concurrent record_asset calls each borrow their own pooled connection;
86
100
  # all succeed with distinct ids (no "another operation in progress").
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes