argus-cortex 0.4.5__tar.gz → 0.4.7__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 (51) hide show
  1. argus_cortex-0.4.7/.cursor/rules/agent-coding-packs.mdc +29 -0
  2. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/.env.example +5 -0
  3. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/.github/workflows/ci.yml +2 -2
  4. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/.github/workflows/release.yml +51 -0
  5. argus_cortex-0.4.7/AGENTS.md +24 -0
  6. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/CHANGELOG.md +37 -1
  7. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/CLAUDE.md +15 -3
  8. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/Makefile +4 -1
  9. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/PKG-INFO +4 -4
  10. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/README.md +3 -3
  11. argus_cortex-0.4.7/SUITE.md +98 -0
  12. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/src/argus_cortex/__init__.py +2 -1
  13. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/src/argus_cortex/_version.py +2 -2
  14. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/src/argus_cortex/moderation/policies.py +29 -14
  15. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/src/argus_cortex/taxonomy.py +19 -2
  16. argus_cortex-0.4.7/taxonomy-schema.json +57 -0
  17. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/tests/test_policies.py +21 -1
  18. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/tests/test_taxonomy.py +18 -2
  19. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/.copier-answers.yml +0 -0
  20. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/.gitignore +0 -0
  21. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/LICENSE +0 -0
  22. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/pyproject.toml +0 -0
  23. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/src/argus_cortex/backends.py +0 -0
  24. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/src/argus_cortex/gpu.py +0 -0
  25. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/src/argus_cortex/moderation/__init__.py +0 -0
  26. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/src/argus_cortex/moderation/_http.py +0 -0
  27. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/src/argus_cortex/moderation/nudenet.py +0 -0
  28. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/src/argus_cortex/moderation/singguard.py +0 -0
  29. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/src/argus_cortex/py.typed +0 -0
  30. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/src/argus_cortex/server.py +0 -0
  31. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/src/argus_cortex/store/__init__.py +0 -0
  32. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/src/argus_cortex/store/blob.py +0 -0
  33. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/src/argus_cortex/store/config.py +0 -0
  34. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/src/argus_cortex/store/errors.py +0 -0
  35. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/src/argus_cortex/store/lineage.py +0 -0
  36. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/src/argus_cortex/store/models.py +0 -0
  37. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/src/argus_cortex/store/schema.py +0 -0
  38. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/src/argus_cortex/store/vector.py +0 -0
  39. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/src/argus_cortex/wire.py +0 -0
  40. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/src/argus_cortex/xmp.py +0 -0
  41. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/tests/test_backends.py +0 -0
  42. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/tests/test_blob.py +0 -0
  43. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/tests/test_gpu.py +0 -0
  44. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/tests/test_moderation.py +0 -0
  45. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/tests/test_server.py +0 -0
  46. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/tests/test_smoke.py +0 -0
  47. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/tests/test_store.py +0 -0
  48. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/tests/test_store_pg_integration.py +0 -0
  49. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/tests/test_vector.py +0 -0
  50. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/tests/test_wire.py +0 -0
  51. {argus_cortex-0.4.5 → argus_cortex-0.4.7}/tests/test_xmp.py +0 -0
@@ -0,0 +1,29 @@
1
+ ---
2
+ description: Fetch ai-code-auditor coding packs for argus-cortex before changing code
3
+ alwaysApply: true
4
+ ---
5
+
6
+ <!-- ai-audit:agent-packs:start -->
7
+ # Agent coding packs
8
+
9
+ Do not wait for a pasted brief — fetch the audit packs for `argus-cortex` yourself.
10
+
11
+ The audit API base URL is `$AI_AUDIT_PUBLIC_BASE_URL`. The real host/IP is in the local `.env`
12
+ (see `.env.example`, placeholder `http://<AUDIT_HOST>:8765`); read it there, don't guess.
13
+
14
+ 1. **Bundle (one call):** `GET $AI_AUDIT_PUBLIC_BASE_URL/audit/packs/bundle?repo=argus-cortex`
15
+ (world model + ranked specialist packs + token-capped `briefs_markdown`;
16
+ optional `&token_budget=2500&q=<keyword>&limit_specialists=5`).
17
+ CLI equivalent: `ai-audit packs --bundle --repo argus-cortex`.
18
+ 2. **Claim** the specialist pack you pick:
19
+ `POST $AI_AUDIT_PUBLIC_BASE_URL/audit/packs/<id>/status` `{"status":"claimed"}`
20
+ (send `X-API-Token: $AI_AUDIT_API_TOKEN`).
21
+ 3. **Fix:** `ai-audit repair --pack-id <id>` (agent-composer repair loop; fetches the pack for you;
22
+ HTTP: `POST http://127.0.0.1:9150/api/repair/run` `{"pack_id":"<id>","mode":"review"}`),
23
+ or read `GET $AI_AUDIT_PUBLIC_BASE_URL/audit/packs/<id>` / `.../brief` and implement it yourself.
24
+ 4. **Close:** `POST $AI_AUDIT_PUBLIC_BASE_URL/audit/validate_patch` `{"repo":"argus-cortex","patch":"<diff>","pack_id":"<id>"}`
25
+ — a passing run marks the pack `resolved`.
26
+
27
+ World-model constraints (ownership, boundaries, scan branch `dev`) win over guesswork.
28
+ Full flow: ai-code-auditor `AGENTS.md`.
29
+ <!-- ai-audit:agent-packs:end -->
@@ -19,3 +19,8 @@ CORTEX_S3_ACCESS_KEY=
19
19
  CORTEX_S3_SECRET_KEY=
20
20
  CORTEX_S3_BUCKET=argus
21
21
  CORTEX_S3_REGION=us-east-1
22
+
23
+ # ── Agent tooling (not read by this service) ──
24
+ # ai-code-auditor audit API base that AGENTS.md tells coding agents to use.
25
+ # Put the real host/IP in your local .env, never here.
26
+ # AI_AUDIT_PUBLIC_BASE_URL=http://<AUDIT_HOST>:8765
@@ -2,9 +2,9 @@ name: CI
2
2
 
3
3
  on:
4
4
  push:
5
- branches: [main]
5
+ branches: [dev, main]
6
6
  pull_request:
7
- branches: [main]
7
+ branches: [dev, main]
8
8
 
9
9
  concurrency:
10
10
  group: ci-${{ github.ref }}
@@ -118,3 +118,54 @@ jobs:
118
118
  else
119
119
  gh release create "$tag" dist/* --verify-tag --title "$tag" --notes-file notes.md $prerelease
120
120
  fi
121
+
122
+ dispatch-stages:
123
+ # Each stage image installs argus-cortex at build time from its pyproject
124
+ # range, so a new cortex only reaches running services once the images are
125
+ # rebuilt. Tell the stages: their release.yml rebuilds the image of their
126
+ # newest release on `cortex-released`. atlas and survey are left out on
127
+ # purpose: they `uv sync --locked`, so a rebuild reinstalls the cortex their
128
+ # lockfile pins — they need a lock bump, not a rebuild. quarry has no cortex.
129
+ runs-on: ubuntu-latest
130
+ needs: publish-pypi
131
+ if: "!cancelled() && needs.publish-pypi.result == 'success'"
132
+ steps:
133
+ - name: Wait until pip can resolve the new version
134
+ # The upload is accepted before the simple index serves it; a stage
135
+ # build that starts in that window would resolve the previous cortex.
136
+ run: |
137
+ version="${GITHUB_REF_NAME#v}"
138
+ wheel="argus_cortex-${version}-py3-none-any.whl"
139
+ for _ in $(seq 60); do
140
+ if curl -fsS https://pypi.org/simple/argus-cortex/ | grep -q "$wheel"; then
141
+ exit 0
142
+ fi
143
+ sleep 10
144
+ done
145
+ echo "::error::$wheel is not on the PyPI simple index after 10 minutes; no stage was told."
146
+ exit 1
147
+ - name: Dispatch cortex-released to the stages
148
+ # A pre-release is skipped: no stage range admits one, so the rebuild
149
+ # would reinstall the cortex already there.
150
+ # SUITE_DISPATCH_TOKEN: a fine-grained PAT with Contents: read & write
151
+ # on the repos below (GITHUB_TOKEN cannot reach another repo).
152
+ env:
153
+ GH_TOKEN: ${{ secrets.SUITE_DISPATCH_TOKEN }}
154
+ run: |
155
+ tag="${GITHUB_REF_NAME}"
156
+ case "$tag" in *-*|*rc*|*a[0-9]*|*b[0-9]*) echo "Pre-release $tag: not dispatching."; exit 0 ;; esac
157
+ if [ -z "$GH_TOKEN" ]; then
158
+ echo "::error::SUITE_DISPATCH_TOKEN is not set; no stage image was rebuilt for $tag."
159
+ exit 1
160
+ fi
161
+ failed=0
162
+ for repo in argus-curator argus-lens argus-proof argus-forge argus-darkroom; do
163
+ if gh api "repos/${GITHUB_REPOSITORY_OWNER}/${repo}/dispatches" \
164
+ -f event_type=cortex-released -f "client_payload[version]=${tag#v}"; then
165
+ echo "Dispatched cortex-released to $repo"
166
+ else
167
+ echo "::error::Could not dispatch to $repo"
168
+ failed=1
169
+ fi
170
+ done
171
+ exit "$failed"
@@ -0,0 +1,24 @@
1
+ <!-- ai-audit:agent-packs:start -->
2
+ ## Agent coding packs (ai-code-auditor)
3
+
4
+ Do not wait for a pasted brief — fetch the audit packs for `argus-cortex` yourself.
5
+
6
+ The audit API base URL is `$AI_AUDIT_PUBLIC_BASE_URL`. The real host/IP is in the local `.env`
7
+ (see `.env.example`, placeholder `http://<AUDIT_HOST>:8765`); read it there, don't guess.
8
+
9
+ 1. **Bundle (one call):** `GET $AI_AUDIT_PUBLIC_BASE_URL/audit/packs/bundle?repo=argus-cortex`
10
+ (world model + ranked specialist packs + token-capped `briefs_markdown`;
11
+ optional `&token_budget=2500&q=<keyword>&limit_specialists=5`).
12
+ CLI equivalent: `ai-audit packs --bundle --repo argus-cortex`.
13
+ 2. **Claim** the specialist pack you pick:
14
+ `POST $AI_AUDIT_PUBLIC_BASE_URL/audit/packs/<id>/status` `{"status":"claimed"}`
15
+ (send `X-API-Token: $AI_AUDIT_API_TOKEN`).
16
+ 3. **Fix:** `ai-audit repair --pack-id <id>` (agent-composer repair loop; fetches the pack for you;
17
+ HTTP: `POST http://127.0.0.1:9150/api/repair/run` `{"pack_id":"<id>","mode":"review"}`),
18
+ or read `GET $AI_AUDIT_PUBLIC_BASE_URL/audit/packs/<id>` / `.../brief` and implement it yourself.
19
+ 4. **Close:** `POST $AI_AUDIT_PUBLIC_BASE_URL/audit/validate_patch` `{"repo":"argus-cortex","patch":"<diff>","pack_id":"<id>"}`
20
+ — a passing run marks the pack `resolved`.
21
+
22
+ World-model constraints (ownership, boundaries, scan branch `dev`) win over guesswork.
23
+ Full flow: ai-code-auditor `AGENTS.md`.
24
+ <!-- ai-audit:agent-packs:end -->
@@ -7,6 +7,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.7] - 2026-11-13
11
+
12
+ ### Added
13
+ - **`concept` target category.** `TargetCategory` gains `concept`, a style or
14
+ visual-concept LoRA, matching argus-quarry's `concept` subjects. It is
15
+ additive, but a stage that keys tables by category (forge's heuristics, curator's
16
+ scanner) must add an entry before it accepts it.
17
+ - **Committed `taxonomy-schema.json`.** Generated from `TargetProfile` by the new
18
+ `taxonomy_schema()` (`make schema`), with a test that fails when it drifts from
19
+ `taxonomy.py`, so non-Python clients such as studio generate their types instead
20
+ of copying the literals (#29).
21
+ - **`SUITE.md`, the suite map for agents.** Every Argus repo, its port and purpose,
22
+ and a "Need it? Use it" index. Each repo's `CLAUDE.md` imports it (#33).
23
+
24
+ ## [0.4.6] - 2026-11-06
25
+
26
+ ### Changed
27
+ - **`adult-cinematic` passes depicted crime without a film cue.** A robbery,
28
+ kidnapping, armed standoff, drugs or war is Safe unless it is sexual, shows
29
+ gore or dismemberment, or teaches how to do it. The crimes heading at B is
30
+ gone, because it was read as any crime shown.
31
+ - **`adult-cinematic` letters changed.** B to F are now Unethical Behavior,
32
+ Cybersecurity, Agent Safety, Politically Sensitive and Animal Abuse, and G is
33
+ Criminal Instructions, so B to G no longer match the other adult policies. A
34
+ filter that names a letter selects a different rule here, including on
35
+ verdicts recorded earlier. Illegal A is unchanged.
36
+
37
+ ### Added
38
+ - **A release now rebuilds the stage images.** Once the version is on the PyPI index,
39
+ `release.yml` sends `cortex-released` to curator, lens, proof, forge and darkroom, whose
40
+ release workflows rebuild their newest release's image against it. Needs the
41
+ `SUITE_DISPATCH_TOKEN` secret (a fine-grained PAT, Contents: read & write on those repos).
42
+ atlas and survey are not sent it: they install from their lockfile.
43
+
10
44
  ## [0.4.5] - 2026-10-31
11
45
 
12
46
  ### Added
@@ -195,7 +229,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
195
229
  (`LocalBackend`, `RemoteBackend`, `RemoteProvider`, `with_retries`
196
230
  retrying only transient errors, `resolve_device`).
197
231
 
198
- [Unreleased]: https://github.com/smk762/argus-cortex/compare/v0.4.5...HEAD
232
+ [Unreleased]: https://github.com/smk762/argus-cortex/compare/v0.4.7...HEAD
233
+ [0.4.7]: https://github.com/smk762/argus-cortex/compare/v0.4.6...v0.4.7
234
+ [0.4.6]: https://github.com/smk762/argus-cortex/compare/v0.4.5...v0.4.6
199
235
  [0.4.5]: https://github.com/smk762/argus-cortex/compare/v0.4.4...v0.4.5
200
236
  [0.4.4]: https://github.com/smk762/argus-cortex/compare/v0.4.3...v0.4.4
201
237
  [0.4.3]: https://github.com/smk762/argus-cortex/compare/v0.4.2...v0.4.3
@@ -4,17 +4,25 @@ Guidance for AI agents working in this repo. Human-facing usage lives in [README
4
4
 
5
5
  ## What this is
6
6
 
7
- The **shared foundation library** of the Argus suite (quarry → curator → lens → forge → proof). It holds the code the stages *share* so it lives in one place instead of copy-pasted per repo: the target taxonomy, the wire-schema versioning discipline, the local/remote AI-backend contract, and the optional stateful-services layer.
7
+ The **shared foundation library** of the Argus suite. It holds the code the stages *share* so it lives in one place instead of copy-pasted per repo: the target taxonomy, the wire-schema versioning discipline, the local/remote AI-backend contract, and the optional stateful-services layer.
8
8
 
9
9
  Naming: the working directory is `argus-core`, but the GitHub repo, the PyPI package, and the import name are all **`argus-cortex`** (`src/argus_cortex/`). Don't "fix" the mismatch.
10
10
 
11
11
  Because everything here is imported by the downstream stages, **a change ripples across the whole suite** — treat this repo as an API, not a leaf. Read [README.md](README.md) for the *why* and worked examples; it is the source of truth for behaviour.
12
12
 
13
+ ## Argus suite
14
+ <!-- argus-suite: keep this block; argus-ci checks for it -->
15
+ Cortex owns the suite map, [SUITE.md](SUITE.md), which every repo's CLAUDE.md imports. **Keep it accurate:** a cortex
16
+ release that adds or changes a shared module updates its "Need it? Use it" row, and PRs from other repos that update
17
+ their own rows land here. Keep it short, because it is loaded into every session in every repo.
18
+
19
+ @SUITE.md
20
+
13
21
  ## Layout
14
22
 
15
23
  `src/argus_cortex/` (top level is pydantic-only; everything heavier is behind an extra and lazily imported):
16
24
 
17
- - `taxonomy.py` — the "moat" types every stage inherits verbatim: `TargetProfile`, `TargetStyle`/`TargetCategory` literals. Keep it dependency-light; a change here retrains everyone's expectations.
25
+ - `taxonomy.py` — the "moat" types every stage inherits verbatim: `TargetProfile`, `TargetStyle`/`TargetCategory` literals, and `taxonomy_schema()` (the source of the committed `taxonomy-schema.json`). Keep it dependency-light; a change here retrains everyone's expectations.
18
26
  - `wire.py` — the versioned wire-schema toolkit consumers build their contracts on: `make_versioned_base(field_name, current, majors)` (a Pydantic base that stamps + version-checks a field like `proof_version`/`manifest_version`), `check_version()` (the major-compatibility gate), `render_schema()`/`wire_schema()` (the canonical committed-schema string behind each consumer's `schema --check` CLI), and `VersionError`.
19
27
  - `backends.py` — the AI-backend contract: `Backend`/`LocalBackend`/`RemoteBackend` (point at a hosted service via `from_host(host, port)` or `from_provider(RemoteProvider.…)`), plus `with_retries()` and `resolve_device()`. httpx is lazy, behind `[remote]`.
20
28
  - `server.py` — the shared FastAPI/ASGI scaffolding other repos' micro-servers mount: `WriteGuard` (pure-ASGI method gate), the `constant_refuse()` (read-only/replay) and `cross_site_refuse()` (cross-origin write) predicates, `env_flag()`, and the `UNSAFE_METHODS`/`TRUTHY`/`FALSY` constants. Starlette is behind `[server]`.
@@ -23,7 +31,7 @@ Because everything here is imported by the downstream stages, **a change ripples
23
31
  - `moderation/` — content-moderation clients over HTTP: `nudenet.py` (`NudeNetClient`, `NsfwResult`, `DEFAULT_EXPOSED_LABELS` — **the suite's one definition of nudity; don't widen it casually**, every stage's NSFW numbers move with it), `singguard.py` (`SingGuardClient` incl. text-only `check_text()`, `GuardResult`, `rule_matches`), `policies.py` (the named policy texts, the per-policy illegal-rule table and `mode()`: `default`/`sfw-*` are SFW — **a policy's text and its illegal letters change together**), `_http.py` (shared POST, proxy-free opener, `CheckFailed`/`ModerationConfigError`). Stdlib + pydantic; Pillow lazily, behind `[images]`. Never import NudeNet (AGPL) — HTTP only. URLs are passed in; stages own their env config.
24
32
  - `store/` — the optional stateful-services layer, opt-in per `CORTEX_*` env var: `lineage.py` (Phase 1 Postgres DAG, `open_lineage_store()`), `vector.py` (Phase 2 Qdrant, `open_vector_store()`), `blob.py` (Phase 3 MinIO/S3, `open_blob_store()`), over `config.py`/`models.py`/`errors.py`. Each `open_*()` returns a `Null*` no-op when its URL is unset. Drivers are behind `[postgres]`/`[qdrant]`/`[s3]`.
25
33
 
26
- There is **no CLI and no committed schema in this repo** — the `schema`/`schema --check` pattern is something `wire.py` *enables* for downstream repos, not something cortex runs on itself.
34
+ There is **no CLI in this repo**. The one committed schema is `taxonomy-schema.json` (from `taxonomy_schema()`; `make schema` regenerates it and `tests/test_taxonomy.py` fails when it is stale). Studio generates its taxonomy types from it, so a `taxonomy.py` change needs `make schema` in the same commit.
27
35
 
28
36
  ## Commands
29
37
 
@@ -45,6 +53,10 @@ Run one test: `uv run --no-sync pytest tests/test_wire.py::test_name -q`.
45
53
  - **Versioning is git-tag-derived** (`hatch-vcs`). Never hand-edit a version; `src/argus_cortex/_version.py` is generated (gitignored). Tag `vX.Y.Z` to release.
46
54
  - Pydantic v2 everywhere; async tests via `asyncio_mode = auto`. Ruff pinned **0.15.16**, line-length 120, target py311.
47
55
 
56
+ ## Branches
57
+
58
+ `dev` is the default branch and the base for every PR. Cut feature branches from `origin/dev` and bring them up to date by merging `origin/dev`, never `main`. Never open a PR against `main` or push work to it directly: `main` must never be ahead of `dev`. A release fast-forwards `main` to a `dev` commit that passed CI (`git push origin origin/dev:main`), then tags it. If `main` is protected and needs a PR, merge the release PR, then immediately merge `main` back into `dev` so the merge commit is not left on `main` alone.
59
+
48
60
  ## CI / release
49
61
 
50
62
  CI runs via the shared [`argus-ci`](https://github.com/smk762/argus-ci) reusable workflow (ruff + pytest with the `dev` extra — which installs the real psycopg/qdrant/minio/starlette clients so each store's live driver path is exercised, not just fakes). Release publishes to PyPI (OIDC trusted publishing) on `v*` tags — **no container image, this is a library**. Scaffolded from [`argus-pkg-template`](https://github.com/smk762/argus-pkg-template); run `copier update` to pull tooling changes.
@@ -1,6 +1,6 @@
1
1
  UV ?= uv
2
2
 
3
- .PHONY: help install lint format test build clean
3
+ .PHONY: help install lint format test schema build clean
4
4
 
5
5
  help: ## Show this help
6
6
  @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-12s\033[0m %s\n", $$1, $$2}'
@@ -20,6 +20,9 @@ format: ## Ruff autoformat + autofix
20
20
  test: ## Run the test suite
21
21
  $(UV) run --no-sync pytest --tb=short -q
22
22
 
23
+ schema: ## Regenerate the committed taxonomy-schema.json
24
+ $(UV) run --no-sync python -c "from pathlib import Path; from argus_cortex.taxonomy import taxonomy_schema; Path('taxonomy-schema.json').write_text(taxonomy_schema(), encoding='utf-8')"
25
+
23
26
  build: ## Build sdist + wheel
24
27
  $(UV) build
25
28
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: argus-cortex
3
- Version: 0.4.5
3
+ Version: 0.4.7
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
@@ -51,11 +51,11 @@ Description-Content-Type: text/markdown
51
51
 
52
52
  Shared foundation for the Argus suite: taxonomy, versioned wire-schema tooling, and the local/remote AI backend contract.
53
53
 
54
- Part of the [Argus suite](https://github.com/smk762?tab=repositories&q=argus) (quarry → curator → lens → forge → proof). This is the library the stages import so the code they *share* — the target taxonomy, the wire-schema versioning discipline, and the way they call local or hosted models — lives in one place instead of being copy-pasted per repo.
54
+ Part of the Argus suite; [SUITE.md](https://github.com/smk762/argus-cortex/blob/dev/SUITE.md) lists every repo, what it owns, and how they fit together. This is the library the stages import so the code they *share* — the target taxonomy, the wire-schema versioning discipline, and the way they call local or hosted models — lives in one place instead of being copy-pasted per repo.
55
55
 
56
56
  ## What's here
57
57
 
58
- - **`argus_cortex.taxonomy`** — `TargetProfile` / `TargetStyle` / `TargetCategory`, the "moat" types every stage inherits verbatim.
58
+ - **`argus_cortex.taxonomy`** — `TargetProfile` / `TargetStyle` / `TargetCategory`, the "moat" types every stage inherits verbatim. `taxonomy_schema()` renders them into the committed `taxonomy-schema.json` (`make schema` regenerates it; a test fails when it is stale), so non-Python clients generate their types instead of copying the literals.
59
59
  - **`argus_cortex.wire`** — the versioned wire-schema toolkit: `check_version()` (major-compatibility gate), `make_versioned_base()` (a Pydantic base that stamps + checks a version field like `proof_version` / `manifest_version`), and `wire_schema()` / `render_schema()` for the committed-schema `schema --check` CLI pattern.
60
60
  - **`argus_cortex.backends`** — the AI-backend contract generalised from argus-lens: `Backend` / `LocalBackend` / `RemoteBackend` (point at a hosted service by `base_url`, `host`/`port`, or a known `RemoteProvider`), plus `with_retries()` and `resolve_device()`. httpx lives behind the `[remote]` extra.
61
61
  - **`argus_cortex.server`** — shared write-guard + env-flag scaffolding for the suite's FastAPI micro-servers: `WriteGuard` (a pure-ASGI method-gate that never buffers streaming responses), the `constant_refuse()` / `cross_site_refuse()` predicates (read-only/replay mode and cross-site-write protection are two configs of the one guard), the `UNSAFE_METHODS` / `TRUTHY` / `FALSY` constants, and `env_flag()` (warns on a mistyped protection flag rather than silently disabling it). Starlette lives behind the `[server]` extra.
@@ -75,7 +75,7 @@ from argus_cortex.backends import RemoteBackend, RemoteProvider
75
75
  _Versioned = make_versioned_base("proof_version", "1.0", ("1",))
76
76
 
77
77
  # reach a hosted scorer by IP/port, or a known provider
78
- scorer = RemoteBackend.from_host("192.168.1.20", 9000, api_key="…")
78
+ scorer = RemoteBackend.from_host("<LAN_HOST>", 9000, api_key="…") # real host/IP: local .env
79
79
  nim = RemoteBackend.from_provider(RemoteProvider.NVIDIA_NIM, api_key="…")
80
80
  ```
81
81
 
@@ -2,11 +2,11 @@
2
2
 
3
3
  Shared foundation for the Argus suite: taxonomy, versioned wire-schema tooling, and the local/remote AI backend contract.
4
4
 
5
- Part of the [Argus suite](https://github.com/smk762?tab=repositories&q=argus) (quarry → curator → lens → forge → proof). This is the library the stages import so the code they *share* — the target taxonomy, the wire-schema versioning discipline, and the way they call local or hosted models — lives in one place instead of being copy-pasted per repo.
5
+ Part of the Argus suite; [SUITE.md](https://github.com/smk762/argus-cortex/blob/dev/SUITE.md) lists every repo, what it owns, and how they fit together. This is the library the stages import so the code they *share* — the target taxonomy, the wire-schema versioning discipline, and the way they call local or hosted models — lives in one place instead of being copy-pasted per repo.
6
6
 
7
7
  ## What's here
8
8
 
9
- - **`argus_cortex.taxonomy`** — `TargetProfile` / `TargetStyle` / `TargetCategory`, the "moat" types every stage inherits verbatim.
9
+ - **`argus_cortex.taxonomy`** — `TargetProfile` / `TargetStyle` / `TargetCategory`, the "moat" types every stage inherits verbatim. `taxonomy_schema()` renders them into the committed `taxonomy-schema.json` (`make schema` regenerates it; a test fails when it is stale), so non-Python clients generate their types instead of copying the literals.
10
10
  - **`argus_cortex.wire`** — the versioned wire-schema toolkit: `check_version()` (major-compatibility gate), `make_versioned_base()` (a Pydantic base that stamps + checks a version field like `proof_version` / `manifest_version`), and `wire_schema()` / `render_schema()` for the committed-schema `schema --check` CLI pattern.
11
11
  - **`argus_cortex.backends`** — the AI-backend contract generalised from argus-lens: `Backend` / `LocalBackend` / `RemoteBackend` (point at a hosted service by `base_url`, `host`/`port`, or a known `RemoteProvider`), plus `with_retries()` and `resolve_device()`. httpx lives behind the `[remote]` extra.
12
12
  - **`argus_cortex.server`** — shared write-guard + env-flag scaffolding for the suite's FastAPI micro-servers: `WriteGuard` (a pure-ASGI method-gate that never buffers streaming responses), the `constant_refuse()` / `cross_site_refuse()` predicates (read-only/replay mode and cross-site-write protection are two configs of the one guard), the `UNSAFE_METHODS` / `TRUTHY` / `FALSY` constants, and `env_flag()` (warns on a mistyped protection flag rather than silently disabling it). Starlette lives behind the `[server]` extra.
@@ -26,7 +26,7 @@ from argus_cortex.backends import RemoteBackend, RemoteProvider
26
26
  _Versioned = make_versioned_base("proof_version", "1.0", ("1",))
27
27
 
28
28
  # reach a hosted scorer by IP/port, or a known provider
29
- scorer = RemoteBackend.from_host("192.168.1.20", 9000, api_key="…")
29
+ scorer = RemoteBackend.from_host("<LAN_HOST>", 9000, api_key="…") # real host/IP: local .env
30
30
  nim = RemoteBackend.from_provider(RemoteProvider.NVIDIA_NIM, api_key="…")
31
31
  ```
32
32
 
@@ -0,0 +1,98 @@
1
+ # The Argus suite: map for agents
2
+
3
+ The canonical list of every Argus repo: what each one is for, and which one already does the thing you are about to build.
4
+ Every repo's `CLAUDE.md` imports this file. **Before designing a feature, check the "Need it? Use it" index.** If a
5
+ sibling already does it, call that sibling (HTTP, or its library) instead of rebuilding it.
6
+
7
+ **Keep it current. Every release of any Argus repo checks its row here.** If a release adds, removes or renames a
8
+ capability, endpoint, port, UI pairing or dependency, open a PR to `argus-cortex` that updates this file in the same
9
+ release. A new repo adds a row and the import block (see the end of this file) before its first release.
10
+
11
+ ## Repos
12
+
13
+ GitHub: `smk762/<repo>`. Ports are each service's default.
14
+
15
+ | Repo | Kind | Port | What it is for |
16
+ |---|---|---|---|
17
+ | **argus-cortex** | library | n/a | Shared foundation: taxonomy, wire-schema versioning, AI-backend contract, server guards, GPU lease, XMP, moderation clients, optional lineage/vector/blob stores. |
18
+ | **argus-quarry** | service | 8102 | Acquire public-domain / CC0 images with provenance and licence into a category-sorted raw pool. Acquisition only. |
19
+ | **argus-curator** | service | 8101 | Decide which images belong in a LoRA training set (quality, near-dup, faces, NSFW, diversity) and export them with a `manifest.jsonl`. |
20
+ | **argus-lens** | service | 8100 (8080 standalone) | Structured, category-bucketed captions, `.txt` sidecars and XMP keywords. |
21
+ | **argus-forge** | service | 8103 | Turn a curated, captioned export into runnable kohya_ss / OneTrainer / diffusers configs. |
22
+ | **argus-proof** | service | 8104 | Evaluate a trained LoRA against its dataset: sample grid, scores, pass/fail. |
23
+ | **argus-atlas** | service | 8105 | Bake georeferenced fantasy-map overlays from real terrain (GDAL controls plus a cartography LoRA), to GeoTIFF / PMTiles. |
24
+ | **argus-darkroom** | service | 8106 | Guarded image generation: checks the prompt, generates via ComfyUI, checks each output, and keeps a record of every attempt. |
25
+ | **argus-survey** | service | 8107 | Point-cloud deliverables for land surveyors: QA, ground, DTM/DSM, contours, volumes, change, ASPRS accuracy. |
26
+ | **argus-mint** | service | 8108 | Provenance tokens for game assets: renders reviewed templates through darkroom, pins to IPFS, mints ERC-721 cards and soulbound deeds, anchors Merkle roots (Base). |
27
+ | **argus-mask** | service | 8109 | A talking face: audio in, facial-animation frames out, rendered in a browser on any LAN screen. |
28
+ | **argus-studio** | UI + compose | 3000 | The suite web UI, and the compose stack that runs the services from GHCR images. No backend code. |
29
+ | **argus-safelight** | UI | 3097 | Demo UI for darkroom: prompt, verdicts, generation over SSE, history. |
30
+ | **argus-cull** | UI | 3000 | Demo UI for curator: which images make the set and why, plus the paths to act on. |
31
+ | **argus-lightbox** | UI (static) | 3000 | Demo gallery navigated by lens captions; bakes from cortex stores, then runs with no server. |
32
+ | **argus-sightline** | UI | 3107 | Workspace UI for survey. |
33
+ | **argus-assay** | UI | 3108 | Review UI for mint: the moderator and owner approve every card, template and frame. |
34
+ | **argus-halo** | infra | n/a | Terraform for the public demo (`argus.dragonhound.dev`): read-only replay services plus a private data host. |
35
+ | **argus-ci** | infra | n/a | Reusable GitHub Actions workflows (uv, ruff, pytest, build, release). |
36
+ | **argus-pkg-template** | infra | n/a | Copier template for new Argus Python packages; `copier update` pulls tooling changes. |
37
+
38
+ **How they fit.** The LoRA pipeline is `quarry → curator → lens → forge → (trainer) → proof → atlas`; curator, lens,
39
+ forge and proof carry the same `TargetProfile` from cortex. Darkroom is the suite's way to generate an image: mint
40
+ renders through it, and anything new that needs a picture (mask backgrounds, game art) should too. Proof's sample grids
41
+ and atlas's bakes predate it and still drive their own diffusion backend. Survey stands alone; its GeoTIFFs can feed
42
+ atlas. Every Python service except quarry depends on cortex. UIs are thin clients: the service decides and the UI only
43
+ shows.
44
+
45
+ **API and UI pairs.** darkroom ↔ safelight · curator ↔ cull · lens ↔ lightbox · survey ↔ sightline · mint ↔ assay.
46
+ Studio has a page for lens, curator, quarry, forge, proof, atlas and darkroom, and links out to sightline.
47
+
48
+ ## Need it? Use it
49
+
50
+ | Need | Use | Don't |
51
+ |---|---|---|
52
+ | Generate an image (a plate, a background, art, a sample) | **darkroom** `POST /generate/stream` (SSE), `GET /image/{id}`. Its record id, model and seed are your provenance. Example client: mint's `DarkroomClient`. | Call ComfyUI directly or generate without checks. |
53
+ | Check an image for nudity, or text/an image against a policy | `argus_cortex.moderation` (`NudeNetClient`, `SingGuardClient`, `policies`) | Write your own nudity definition or policy text. |
54
+ | Share the GPU with other services | `argus_cortex.gpu` (`coordinator_from_env()`, `lease()`) | Assume the GPU is free. |
55
+ | Call a local or hosted model | `argus_cortex.backends` (`LocalBackend`, `RemoteBackend`, `with_retries`) | Hand-roll an HTTP client per model. |
56
+ | Version a wire payload, commit its schema | `argus_cortex.wire` (`make_versioned_base`, `render_schema`) | Ship an unversioned contract. |
57
+ | Guard a FastAPI server's writes, or run read-only | `argus_cortex.server` (`WriteGuard`, `env_flag`) | Use `BaseHTTPMiddleware`, which buffers streams. |
58
+ | Read or write XMP sidecars | `argus_cortex.xmp` | Overwrite another stage's properties. |
59
+ | Record lineage, embeddings or blobs | `argus_cortex.store` (`open_lineage_store` / `open_vector_store` / `open_blob_store`) | Add a database to a single stage. |
60
+ | The training target (style, category) | `argus_cortex.taxonomy`; non-Python clients use `taxonomy-schema.json` | Copy the literals. |
61
+ | Acquire source photos with licence and provenance | **quarry** | Scrape inside another stage. |
62
+ | Score, dedupe or select images; faces; content rating | **curator** | Re-score in a later stage. |
63
+ | Caption or keyword images | **lens** | Write ad-hoc captions. |
64
+ | Training configs, or evaluating a LoRA | **forge**, **proof** | |
65
+ | Terrain, georeferencing, map tiles | **atlas** (stylised), **survey** (survey-grade) | |
66
+ | A face that speaks | **mask** | |
67
+ | On-chain provenance, IPFS | **mint** | |
68
+ | Run the whole stack locally | **studio** `compose.yaml` | |
69
+ | A new Python package, or CI | **argus-pkg-template**, **argus-ci** | Copy another repo's tooling by hand. |
70
+
71
+ ## Shared rules
72
+
73
+ - Shared code lives in cortex: import it, don't copy it. A cortex change ripples into every stage on its next bump.
74
+ - Provenance first. Anything the suite serves or trains on carries its source and licence; unlicensed input is refused.
75
+ - Optional heavy dependencies are lazy imports behind extras. A missing service config fails open to a no-op, never a crash.
76
+ - Talk across repos over HTTP or a versioned wire schema, never by reading another repo's files.
77
+
78
+ ## Local checkouts
79
+
80
+ The maintainer's siblings live under `~/`, so the paths are `../argus-<name>`, with two exceptions:
81
+ - cortex is `../argus-core`;
82
+ - studio is `../argus-vision-demo`.
83
+
84
+ Directories named `argus-<repo>-<topic>` are worktrees of `<repo>`, not separate repos.
85
+
86
+ ## The import block (every repo's CLAUDE.md)
87
+
88
+ ```markdown
89
+ ## Argus suite
90
+ <!-- argus-suite: keep this block; argus-ci checks for it -->
91
+ This repo is part of the Argus suite. The suite map below lists every sibling and what it owns; check it before
92
+ building something a sibling may already do. **On every release, update this repo's row and capabilities in
93
+ argus-cortex's SUITE.md** (a PR to argus-cortex in the same release).
94
+
95
+ @../argus-core/SUITE.md
96
+
97
+ Not checked out? Read https://github.com/smk762/argus-cortex/blob/dev/SUITE.md
98
+ ```
@@ -55,7 +55,7 @@ from argus_cortex.store import (
55
55
  open_lineage_store,
56
56
  open_vector_store,
57
57
  )
58
- from argus_cortex.taxonomy import TargetCategory, TargetProfile, TargetStyle
58
+ from argus_cortex.taxonomy import TargetCategory, TargetProfile, TargetStyle, taxonomy_schema
59
59
  from argus_cortex.wire import (
60
60
  VersionError,
61
61
  check_version,
@@ -83,6 +83,7 @@ __all__ = [
83
83
  "TargetProfile",
84
84
  "TargetCategory",
85
85
  "TargetStyle",
86
+ "taxonomy_schema",
86
87
  # wire
87
88
  "VersionError",
88
89
  "check_version",
@@ -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.5'
22
- __version_tuple__ = version_tuple = (0, 4, 5)
21
+ __version__ = version = '0.4.7'
22
+ __version_tuple__ = version_tuple = (0, 4, 7)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -37,7 +37,7 @@ POLICIES: tuple[PolicyName, ...] = (
37
37
  )
38
38
  SFW_PREFIX = "sfw-"
39
39
 
40
- # Rules B to G, shared by every adult policy except cinematic B: the same
40
+ # Rules B to G, shared by every adult policy except cinematic: the same
41
41
  # letters and areas as SingGuard's defaults. They are our wording, so their
42
42
  # verdicts can differ from the built-ins.
43
43
  _CRIME_B = [
@@ -45,8 +45,10 @@ _CRIME_B = [
45
45
  " - Content involving violent crime, weapons, other crimes, or public-safety threats.",
46
46
  ]
47
47
  # Depiction is not a hit; how-to, a named real target, and operational threats are.
48
- _CRIME_B_CINEMATIC = [
49
- "### B. Real-World Crimes & Public Safety",
48
+ # Last, not at B: a heading called crimes at B was read as any crime shown,
49
+ # whatever its bullet said.
50
+ _CRIME_CINEMATIC = [
51
+ "### G. Criminal Instructions",
50
52
  (
51
53
  " - Instructional or operational assistance for a real-world crime: how to commit one, "
52
54
  "targeting a named real person or a real institution, or an operational public-safety threat."
@@ -69,6 +71,13 @@ _OTHER_AFTER_B = [
69
71
  " - Content involving cruelty to animals or the spread of animal abuse.",
70
72
  ]
71
73
  _OTHER_RULES = [*_CRIME_B, *_OTHER_AFTER_B]
74
+
75
+
76
+ def _one_letter_up(line: str) -> str:
77
+ """A ``### C. ...`` heading as ``### B. ...``; a bullet as it is."""
78
+ return f"### {chr(ord(line[4]) - 1)}{line[5:]}" if line.startswith("### ") else line
79
+
80
+
72
81
  _MINORS = " - Sexual content involving anyone under 18, or anyone who appears to be a minor."
73
82
  _NON_CONSENT = (
74
83
  " - Non-consensual sexual content, including hidden recording, sexual images of a real "
@@ -173,10 +182,13 @@ ADULT_CAUTIOUS_POLICY = "\n".join(
173
182
  ]
174
183
  )
175
184
 
176
- # ADULT_POLICY with B narrowed to how-to / real targeting, and cinematic
177
- # crime named under Safe. Adult entertainment stays Safe the same way as
178
- # under adult. Compared against adult; do not fold this into granular
179
- # (that mix of lettering and B permissiveness is issue #24).
185
+ # ADULT_POLICY with no crimes heading at B: the rules after it move up a
186
+ # letter, and G is how-to / real targeting only. Depicted crime is named under
187
+ # Safe, and adult entertainment stays Safe the same way as under adult.
188
+ # Letters must stay contiguous: with a gap at B the model renumbered the rules
189
+ # in its reasoning, so verdicts named the wrong rule and some changed. Compared
190
+ # against adult; do not fold this into granular (that mix of lettering and
191
+ # crime permissiveness is issue #24).
180
192
  ADULT_CINEMATIC_POLICY = "\n".join(
181
193
  [
182
194
  "## Risk Categories",
@@ -184,8 +196,8 @@ ADULT_CINEMATIC_POLICY = "\n".join(
184
196
  _MINORS,
185
197
  _NON_CONSENT,
186
198
  _EXTREME,
187
- *_CRIME_B_CINEMATIC,
188
- *_OTHER_AFTER_B,
199
+ *(_one_letter_up(line) for line in _OTHER_AFTER_B),
200
+ *_CRIME_CINEMATIC,
189
201
  "### Safe",
190
202
  *_ADULT_SAFE,
191
203
  (
@@ -193,10 +205,12 @@ ADULT_CINEMATIC_POLICY = "\n".join(
193
205
  "commercial porn, studio stills, explicit sex, and an adult-site watermark."
194
206
  ),
195
207
  (
196
- " - Fictional or cinematic crime as a story or still is Safe: a bank robbery, a heist, "
197
- "fraud such as stealing pensions, institutional corruption such as dirty cops, "
198
- "non-sexual violence, weapons shown as a prop, drugs as a plot, and war as a setting."
208
+ " - A depiction of crime or violence is Safe, as in film, television, games, and print, and it "
209
+ "needs no sign that it is fiction: a robbery, a heist, a kidnapping, a person bound or held "
210
+ "captive, theft, fraud, corrupt officials, an armed standoff, a police raid, a fight, a battle, "
211
+ "weapons, drugs, and war."
199
212
  ),
213
+ " - That stops being Safe when it is sexual, shows gore or dismemberment, or teaches how to do it.",
200
214
  ]
201
215
  )
202
216
 
@@ -209,8 +223,9 @@ _TEXT: dict[str, str | None] = {
209
223
  }
210
224
 
211
225
  # Rule letters that mean illegal content. "default" has none: its A covers
212
- # consensual adult nudity too, so it cannot tell illegal from adult. Crime (B)
213
- # and animal abuse (G) depict, they are not illegal content, under every policy.
226
+ # consensual adult nudity too, so it cannot tell illegal from adult. Crime and
227
+ # animal abuse depict, they are not illegal content, under every policy, whatever
228
+ # their letters: B and G, or G and F under adult-cinematic.
214
229
  _ILLEGAL: dict[str, frozenset[str]] = {
215
230
  "default": frozenset(),
216
231
  "adult": frozenset({"A"}),
@@ -16,15 +16,19 @@ from typing import Literal
16
16
 
17
17
  from pydantic import BaseModel
18
18
 
19
+ from argus_cortex.wire import render_schema
20
+
19
21
  TargetStyle = Literal["photo", "anime"]
20
22
 
21
23
  # What the dataset is curated for. Note this is distinct from argus-lens's
22
24
  # caption *buckets* (identity/action/pose_composition/wardrobe/lighting/setting):
23
25
  # those describe caption fragments, this describes the training objective.
24
- TargetCategory = Literal["identity", "wardrobe", "pose_composition", "setting"]
26
+ # ``concept`` is a style or visual-concept LoRA: what argus-quarry acquires
27
+ # under its ``concept`` subjects.
28
+ TargetCategory = Literal["identity", "wardrobe", "pose_composition", "setting", "concept"]
25
29
 
26
30
  TARGET_STYLES: tuple[TargetStyle, ...] = ("photo", "anime")
27
- TARGET_CATEGORIES: tuple[TargetCategory, ...] = ("identity", "wardrobe", "pose_composition", "setting")
31
+ TARGET_CATEGORIES: tuple[TargetCategory, ...] = ("identity", "wardrobe", "pose_composition", "setting", "concept")
28
32
 
29
33
 
30
34
  class TargetProfile(BaseModel):
@@ -39,3 +43,16 @@ class TargetProfile(BaseModel):
39
43
  target_backend: str | None = "sdxl"
40
44
  checkpoint: str | None = None
41
45
  target_category: TargetCategory = "identity"
46
+
47
+
48
+ TAXONOMY_SCHEMA_TITLE = "argus-cortex taxonomy"
49
+
50
+
51
+ def taxonomy_schema() -> str:
52
+ """The canonical JSON Schema for the taxonomy, as committed in ``taxonomy-schema.json``.
53
+
54
+ Non-Python clients (argus-studio) generate their types from that file instead
55
+ of hand-copying the literals. ``tests/test_taxonomy.py`` fails if the
56
+ committed file and this module drift; regenerate it with ``make schema``.
57
+ """
58
+ return render_schema([TargetProfile], title=TAXONOMY_SCHEMA_TITLE)
@@ -0,0 +1,57 @@
1
+ {
2
+ "$defs": {
3
+ "TargetProfile": {
4
+ "description": "What a dataset was curated for \u2014 shared verbatim across the suite.\n\nInherited unchanged from stage to stage (curator \u2192 lens \u2192 forge \u2192 proof) so\na decision made once at curation time is honoured everywhere downstream; no\nstage remaps the taxonomy.",
5
+ "properties": {
6
+ "checkpoint": {
7
+ "anyOf": [
8
+ {
9
+ "type": "string"
10
+ },
11
+ {
12
+ "type": "null"
13
+ }
14
+ ],
15
+ "default": null,
16
+ "title": "Checkpoint"
17
+ },
18
+ "target_backend": {
19
+ "anyOf": [
20
+ {
21
+ "type": "string"
22
+ },
23
+ {
24
+ "type": "null"
25
+ }
26
+ ],
27
+ "default": "sdxl",
28
+ "title": "Target Backend"
29
+ },
30
+ "target_category": {
31
+ "default": "identity",
32
+ "enum": [
33
+ "identity",
34
+ "wardrobe",
35
+ "pose_composition",
36
+ "setting",
37
+ "concept"
38
+ ],
39
+ "title": "Target Category",
40
+ "type": "string"
41
+ },
42
+ "target_style": {
43
+ "default": "photo",
44
+ "enum": [
45
+ "photo",
46
+ "anime"
47
+ ],
48
+ "title": "Target Style",
49
+ "type": "string"
50
+ }
51
+ },
52
+ "title": "TargetProfile",
53
+ "type": "object"
54
+ }
55
+ },
56
+ "title": "argus-cortex taxonomy"
57
+ }
@@ -45,6 +45,9 @@ def test_is_illegal_reads_a_rule_as_verdicts_report_it() -> None:
45
45
  assert not policies.is_illegal(name, "B. Real-World Crimes & Public Safety")
46
46
  assert not policies.is_illegal(name, "G. Animal Abuse")
47
47
  assert not policies.is_illegal(name, "Safe")
48
+ # adult-cinematic's own letters: crime is G and animal abuse F.
49
+ assert not policies.is_illegal("adult-cinematic", "G. Criminal Instructions")
50
+ assert not policies.is_illegal("adult-cinematic", "F. Animal Abuse")
48
51
 
49
52
 
50
53
  def test_adult_safe_names_explicit_commercial_content() -> None:
@@ -65,12 +68,29 @@ def test_adult_cinematic_narrows_crime_and_keeps_adult_entertainment_safe() -> N
65
68
  risks, safe = text.split("### Safe")
66
69
  assert "how to commit one" in risks
67
70
  assert "violent crime, weapons, other crimes" not in risks
68
- assert "bank robbery" in safe and "stealing pensions" in safe and "dirty cops" in safe
71
+ assert "needs no sign that it is fiction" in safe
72
+ assert "a kidnapping" in safe and "drugs" in safe and "war" in safe
73
+ assert "stops being Safe when it is sexual, shows gore or dismemberment" in safe
69
74
  assert "Legal adult entertainment is Safe under this policy, the same as under adult" in safe
70
75
  assert "between adults" not in text
71
76
  assert policies.policy_text("adult-cinematic") != policies.policy_text("adult")
72
77
 
73
78
 
79
+ def test_adult_cinematic_letters_have_no_gap() -> None:
80
+ # With a gap the model renumbers the rules as it reasons, and verdicts name the wrong one.
81
+ headings = [line[4:] for line in policies.policy_text("adult-cinematic").splitlines() if line.startswith("### ")]
82
+ assert headings == [
83
+ "A. Illegal Sexual Content",
84
+ "B. Unethical Behavior",
85
+ "C. Cybersecurity & Information Manipulation",
86
+ "D. Agent Safety",
87
+ "E. Politically Sensitive Content",
88
+ "F. Animal Abuse",
89
+ "G. Criminal Instructions",
90
+ "Safe",
91
+ ]
92
+
93
+
74
94
  def test_mode_is_by_name() -> None:
75
95
  assert policies.mode("default") == "sfw"
76
96
  assert policies.mode("sfw-strict") == "sfw" # a later SFW policy needs no table entry
@@ -1,9 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
4
+ from pathlib import Path
5
+
3
6
  import pytest
4
7
  from pydantic import ValidationError
5
8
 
6
- from argus_cortex.taxonomy import TARGET_CATEGORIES, TARGET_STYLES, TargetProfile
9
+ from argus_cortex.taxonomy import TARGET_CATEGORIES, TARGET_STYLES, TargetProfile, taxonomy_schema
7
10
 
8
11
 
9
12
  def test_defaults() -> None:
@@ -34,4 +37,17 @@ def test_round_trips() -> None:
34
37
 
35
38
  def test_tuples_match_literals() -> None:
36
39
  assert set(TARGET_STYLES) == {"photo", "anime"}
37
- assert set(TARGET_CATEGORIES) == {"identity", "wardrobe", "pose_composition", "setting"}
40
+ assert set(TARGET_CATEGORIES) == {"identity", "wardrobe", "pose_composition", "setting", "concept"}
41
+
42
+
43
+ SCHEMA_FILE = Path(__file__).resolve().parent.parent / "taxonomy-schema.json"
44
+
45
+
46
+ def test_committed_schema_matches_taxonomy() -> None:
47
+ assert SCHEMA_FILE.read_text() == taxonomy_schema(), "taxonomy-schema.json is stale; run `make schema`"
48
+
49
+
50
+ def test_schema_lists_every_literal() -> None:
51
+ defs = json.loads(taxonomy_schema())["$defs"]["TargetProfile"]["properties"]
52
+ assert defs["target_category"]["enum"] == list(TARGET_CATEGORIES)
53
+ assert defs["target_style"]["enum"] == list(TARGET_STYLES)
File without changes
File without changes