civon-image-edit-audit 0.1.0__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.
@@ -0,0 +1,27 @@
1
+ # Dependencies and local tool state
2
+ node_modules/
3
+ .pnpm-store/
4
+ .DS_Store
5
+
6
+ # Local environment files
7
+ .env
8
+ .env.*
9
+ !.env.example
10
+
11
+ # Runtime output is local-only by default: JSON logs, helper scripts, screenshots,
12
+ # browser snapshots, network captures, and lock files all belong here.
13
+ runs/*
14
+ !runs/.gitkeep
15
+
16
+ # A human may deliberately promote a small, redacted JSON receipt as durable
17
+ # evidence. Promotion is explicit by path, not automatic from a runner.
18
+ !runs/evidence/
19
+ runs/evidence/*
20
+ !runs/evidence/README.md
21
+ !runs/evidence/*.json
22
+
23
+ # Target-size media exports are reproducible local cache files. Reviewed shared
24
+ # masters under assets/<site>/_shared/v*/ remain tracked. Add one export with
25
+ # `git add -f` only when an exact reviewed file must survive across machines for
26
+ # a registered publishing asset; never force-add the whole export cache.
27
+ assets/**/_shared/exports/
@@ -0,0 +1,7 @@
1
+ LicenseRef-NoLicenseGrant
2
+
3
+ This prepared package makes no open-source or other package-use license grant.
4
+ No ownership certificate, exclusivity claim, third-party license or permission
5
+ to use product inputs or outputs is asserted. Distribution rights and registry
6
+ publication must be confirmed by the authorised owner before execution.
7
+ Local preparation does not upload or publish this package.
@@ -0,0 +1,81 @@
1
+ Metadata-Version: 2.5
2
+ Name: civon-image-edit-audit
3
+ Version: 0.1.0
4
+ Summary: Audit human image-edit records and quote totals with a dependency-free Python library and CLI.
5
+ Project-URL: Homepage, https://civon.ai/image-editor
6
+ License-Expression: LicenseRef-NoLicenseGrant
7
+ License-File: LICENSE
8
+ Keywords: image-editing,manual-review,record-validation,workflow
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+
14
+ # civon-image-edit-audit
15
+
16
+ A dependency-free Python audit library and local CLI for **human-entered image-edit records**. It validates a JSON interchange format, identifies incomplete checks and failed candidates, counts manual acceptances, and sums recorded credit quotes with decimal arithmetic. It also produces a compact Markdown report.
17
+
18
+ This is an independent workflow utility, not an official Civon generation SDK. It does not generate, inspect, edit, hash or upload images, read an account balance, fetch current prices, run a model or verify rights. The decisions reflect only the supplied human assertions. Original AI-assisted code and text; no private product implementation, image, credential or third-party runtime package is included.
19
+
20
+ ## Local installation and runtime
21
+
22
+ The prepared release contains a real wheel and source distribution. Install the actual local wheel in a virtual environment:
23
+
24
+ ```sh
25
+ python -m pip install --no-index --no-deps /absolute/path/to/civon_image_edit_audit-0.1.0-py3-none-any.whl
26
+ civon-edit-audit records.json
27
+ civon-edit-audit --markdown records.json
28
+ civon-edit-audit - < records.json
29
+ ```
30
+
31
+ Python 3.10 or later is declared. Local preparation tests the installed artifacts on the available Python runtime and records that version; it does not claim a full version matrix. Hatchling and build are build tools, not runtime dependencies. No PyPI or TestPyPI upload, live project URL or name reservation is established by this bundle.
32
+
33
+ ## Python API
34
+
35
+ ```python
36
+ from civon_image_edit_audit import audit_record, audit_records, load_records, to_markdown
37
+
38
+ candidate = {
39
+ "schema": "civon-image-edit-record/v1",
40
+ "id": "draft-1",
41
+ "candidate": "mug-background-v1.png",
42
+ "prompt": "Explore a warmer backdrop; inspect lettering.",
43
+ "settings": "Manually recorded model, ratio and resolution",
44
+ "quotedCredits": "2.5", # synthetic example, not a product price
45
+ "checks": {
46
+ "subject": "pass", "text": "fail", "geometry": "pass",
47
+ "edges": "pass", "lighting": "pass", "delivery": "pass"
48
+ },
49
+ "permissionsConfirmed": False
50
+ }
51
+
52
+ result = audit_record(candidate)
53
+ assert result.status == "revise"
54
+ assert result.failed == ("text",)
55
+ report = audit_records([candidate])
56
+ assert report["quotedCredits"] == "2.5"
57
+ print(to_markdown([candidate]))
58
+ ```
59
+
60
+ These sample checks are fictional and do not describe an actual image analysis or product result. `AuditResult` is a frozen dataclass; audit functions do not mutate the supplied records.
61
+
62
+ ## Interchange contract
63
+
64
+ The input is an array of `civon-image-edit-record/v1` records, compatible with JSON-serialised records from the separately prepared JavaScript record utility. This Python package does not require or execute that JavaScript package.
65
+
66
+ - Required non-empty strings: `id` (up to 160 characters), `candidate` (250), `prompt` (2000), `settings` (2000).
67
+ - `quotedCredits` is a decimal string with at most six whole digits and three fractional digits, or null/omitted for unknown. An explicit `"0"` is distinct from unknown. Numbers, negative values, exponent notation and excessive precision are rejected. Decimal arithmetic totals all known quotes, including rejected candidates; these are entries, not charges, prices or an account balance.
68
+ - Optional `sourceSha256` and `candidateSha256` contain 64 hexadecimal characters or null. Only their syntax is checked; files are not opened or hashed. Optional `notes` is non-empty text up to 2000 characters, or null/omitted.
69
+ - `checks` contains exactly `subject`, `text`, `geometry`, `edges`, `lighting` and `delivery`. Each value is `pending`, `pass`, `fail` or `na`. `permissionsConfirmed` must be an actual boolean, not a string or number.
70
+ - A failed check yields `revise`; a pending check yields `pending`. All N/A is insufficient. Acceptance requires at least one Pass and confirmed permissions after all checks resolve. The permissions field records a user assertion, not legal clearance.
71
+ - Arrays are limited to 10,000 records, with unique trimmed ids. Errors identify the zero-based record index where applicable. `load_records` rejects duplicate JSON keys, non-finite constants and malformed/deeply nested JSON. Unknown top-level extra fields are ignored; unknown check fields are rejected.
72
+
73
+ The CLI reads at most 5 MiB plus one byte to detect oversized input. It accepts one local UTF-8 file or stdin, prints a report and writes no file. Invalid data returns status 1, argument errors 2, and valid reports/help 0. `to_markdown` escapes table delimiters and line breaks; it is not an HTML sanitiser. Keep exported prompts, filenames, settings and notes private when needed.
74
+
75
+ ## Provenance and license posture
76
+
77
+ Metadata uses `LicenseRef-NoLicenseGrant` with the included notice. This preparation grants no open-source or other package-use license and makes no ownership or exclusivity assurance. Account authority, distribution rights, namespace/version availability and final publication remain execution checks. No author, email, source repository or issue tracker is invented.
78
+
79
+ ## Related product context
80
+
81
+ [Civon AI Image Editor](https://civon.ai/image-editor) is a browser-based, prompt-led still-image editor. Model choices, usable reference counts, settings, access and quoted credits vary. This audit utility works without visiting or connecting to it and provides no model integration, endorsement by PyPI or guarantee about an edited image.
@@ -0,0 +1,68 @@
1
+ # civon-image-edit-audit
2
+
3
+ A dependency-free Python audit library and local CLI for **human-entered image-edit records**. It validates a JSON interchange format, identifies incomplete checks and failed candidates, counts manual acceptances, and sums recorded credit quotes with decimal arithmetic. It also produces a compact Markdown report.
4
+
5
+ This is an independent workflow utility, not an official Civon generation SDK. It does not generate, inspect, edit, hash or upload images, read an account balance, fetch current prices, run a model or verify rights. The decisions reflect only the supplied human assertions. Original AI-assisted code and text; no private product implementation, image, credential or third-party runtime package is included.
6
+
7
+ ## Local installation and runtime
8
+
9
+ The prepared release contains a real wheel and source distribution. Install the actual local wheel in a virtual environment:
10
+
11
+ ```sh
12
+ python -m pip install --no-index --no-deps /absolute/path/to/civon_image_edit_audit-0.1.0-py3-none-any.whl
13
+ civon-edit-audit records.json
14
+ civon-edit-audit --markdown records.json
15
+ civon-edit-audit - < records.json
16
+ ```
17
+
18
+ Python 3.10 or later is declared. Local preparation tests the installed artifacts on the available Python runtime and records that version; it does not claim a full version matrix. Hatchling and build are build tools, not runtime dependencies. No PyPI or TestPyPI upload, live project URL or name reservation is established by this bundle.
19
+
20
+ ## Python API
21
+
22
+ ```python
23
+ from civon_image_edit_audit import audit_record, audit_records, load_records, to_markdown
24
+
25
+ candidate = {
26
+ "schema": "civon-image-edit-record/v1",
27
+ "id": "draft-1",
28
+ "candidate": "mug-background-v1.png",
29
+ "prompt": "Explore a warmer backdrop; inspect lettering.",
30
+ "settings": "Manually recorded model, ratio and resolution",
31
+ "quotedCredits": "2.5", # synthetic example, not a product price
32
+ "checks": {
33
+ "subject": "pass", "text": "fail", "geometry": "pass",
34
+ "edges": "pass", "lighting": "pass", "delivery": "pass"
35
+ },
36
+ "permissionsConfirmed": False
37
+ }
38
+
39
+ result = audit_record(candidate)
40
+ assert result.status == "revise"
41
+ assert result.failed == ("text",)
42
+ report = audit_records([candidate])
43
+ assert report["quotedCredits"] == "2.5"
44
+ print(to_markdown([candidate]))
45
+ ```
46
+
47
+ These sample checks are fictional and do not describe an actual image analysis or product result. `AuditResult` is a frozen dataclass; audit functions do not mutate the supplied records.
48
+
49
+ ## Interchange contract
50
+
51
+ The input is an array of `civon-image-edit-record/v1` records, compatible with JSON-serialised records from the separately prepared JavaScript record utility. This Python package does not require or execute that JavaScript package.
52
+
53
+ - Required non-empty strings: `id` (up to 160 characters), `candidate` (250), `prompt` (2000), `settings` (2000).
54
+ - `quotedCredits` is a decimal string with at most six whole digits and three fractional digits, or null/omitted for unknown. An explicit `"0"` is distinct from unknown. Numbers, negative values, exponent notation and excessive precision are rejected. Decimal arithmetic totals all known quotes, including rejected candidates; these are entries, not charges, prices or an account balance.
55
+ - Optional `sourceSha256` and `candidateSha256` contain 64 hexadecimal characters or null. Only their syntax is checked; files are not opened or hashed. Optional `notes` is non-empty text up to 2000 characters, or null/omitted.
56
+ - `checks` contains exactly `subject`, `text`, `geometry`, `edges`, `lighting` and `delivery`. Each value is `pending`, `pass`, `fail` or `na`. `permissionsConfirmed` must be an actual boolean, not a string or number.
57
+ - A failed check yields `revise`; a pending check yields `pending`. All N/A is insufficient. Acceptance requires at least one Pass and confirmed permissions after all checks resolve. The permissions field records a user assertion, not legal clearance.
58
+ - Arrays are limited to 10,000 records, with unique trimmed ids. Errors identify the zero-based record index where applicable. `load_records` rejects duplicate JSON keys, non-finite constants and malformed/deeply nested JSON. Unknown top-level extra fields are ignored; unknown check fields are rejected.
59
+
60
+ The CLI reads at most 5 MiB plus one byte to detect oversized input. It accepts one local UTF-8 file or stdin, prints a report and writes no file. Invalid data returns status 1, argument errors 2, and valid reports/help 0. `to_markdown` escapes table delimiters and line breaks; it is not an HTML sanitiser. Keep exported prompts, filenames, settings and notes private when needed.
61
+
62
+ ## Provenance and license posture
63
+
64
+ Metadata uses `LicenseRef-NoLicenseGrant` with the included notice. This preparation grants no open-source or other package-use license and makes no ownership or exclusivity assurance. Account authority, distribution rights, namespace/version availability and final publication remain execution checks. No author, email, source repository or issue tracker is invented.
65
+
66
+ ## Related product context
67
+
68
+ [Civon AI Image Editor](https://civon.ai/image-editor) is a browser-based, prompt-led still-image editor. Model choices, usable reference counts, settings, access and quoted credits vary. This audit utility works without visiting or connecting to it and provides no model integration, endorsement by PyPI or guarantee about an edited image.
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "civon-image-edit-audit"
7
+ version = "0.1.0"
8
+ description = "Audit human image-edit records and quote totals with a dependency-free Python library and CLI."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "LicenseRef-NoLicenseGrant"
12
+ license-files = ["LICENSE"]
13
+ dependencies = []
14
+ keywords = ["image-editing", "manual-review", "record-validation", "workflow"]
15
+ classifiers = ["Programming Language :: Python :: 3", "Operating System :: OS Independent"]
16
+
17
+ [project.urls]
18
+ Homepage = "https://civon.ai/image-editor"
19
+
20
+ [project.scripts]
21
+ civon-edit-audit = "civon_image_edit_audit.cli:main"
22
+
23
+ [tool.hatch.build.targets.wheel]
24
+ packages = ["src/civon_image_edit_audit"]
25
+
26
+ [tool.hatch.build.targets.sdist]
27
+ include = ["src/", "tests/", "README.md", "LICENSE", "pyproject.toml"]
@@ -0,0 +1,5 @@
1
+ """Audit manual decisions; no image analysis, network I/O or rights verification."""
2
+ from .audit import AuditError, AuditResult, audit_record, audit_records, load_records, to_markdown
3
+
4
+ __version__ = "0.1.0"
5
+ __all__ = ["AuditError", "AuditResult", "audit_record", "audit_records", "load_records", "to_markdown"]
@@ -0,0 +1,150 @@
1
+ """Validate the civon-image-edit-record/v1 JSON interchange format."""
2
+ from dataclasses import dataclass
3
+ from decimal import Decimal, localcontext
4
+ import json
5
+ import re
6
+ from typing import Any
7
+
8
+ CHECKS = ("subject", "text", "geometry", "edges", "lighting", "delivery")
9
+ RESULTS = frozenset(("pending", "pass", "fail", "na"))
10
+ SCHEMA = "civon-image-edit-record/v1"
11
+
12
+
13
+ class AuditError(ValueError):
14
+ """An input violates the documented manual-record contract."""
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class AuditResult:
19
+ record_id: str
20
+ candidate: str
21
+ status: str
22
+ reason: str
23
+ quoted_credits: Decimal | None
24
+ failed: tuple[str, ...]
25
+ pending: tuple[str, ...]
26
+
27
+
28
+ def _text(value: Any, field: str, maximum: int) -> str:
29
+ if not isinstance(value, str) or not value.strip() or len(value) > maximum:
30
+ raise AuditError(f"{field} must be non-empty text (maximum {maximum} characters)")
31
+ return value.strip()
32
+
33
+
34
+ def _quote(value: Any) -> Decimal | None:
35
+ if value is None:
36
+ return None
37
+ if not isinstance(value, str) or not re.fullmatch(r"[0-9]{1,6}(?:\.[0-9]{1,3})?", value):
38
+ raise AuditError("quotedCredits must be a decimal string with up to 6 whole digits and 3 decimal places, or null")
39
+ return Decimal(value)
40
+
41
+
42
+ def _amount(value: Decimal) -> str:
43
+ text = format(value, "f")
44
+ return text.rstrip("0").rstrip(".") if "." in text else text
45
+
46
+
47
+ def audit_record(record: Any) -> AuditResult:
48
+ """Validate a record and derive status from the supplied human assertions."""
49
+ if not isinstance(record, dict) or record.get("schema") != SCHEMA:
50
+ raise AuditError("Unsupported image-edit record schema")
51
+ record_id = _text(record.get("id"), "id", 160)
52
+ candidate = _text(record.get("candidate"), "candidate", 250)
53
+ _text(record.get("prompt"), "prompt", 2000)
54
+ _text(record.get("settings"), "settings", 2000)
55
+ quote = _quote(record.get("quotedCredits"))
56
+ for name in ("sourceSha256", "candidateSha256"):
57
+ value = record.get(name)
58
+ if value is not None and (not isinstance(value, str) or not re.fullmatch(r"[a-fA-F0-9]{64}", value)):
59
+ raise AuditError(f"{name} must be SHA-256 hexadecimal text or null")
60
+ if record.get("notes") is not None:
61
+ _text(record["notes"], "notes", 2000)
62
+ checks = record.get("checks")
63
+ if not isinstance(checks, dict) or set(checks) != set(CHECKS):
64
+ raise AuditError("checks must contain exactly the six documented names")
65
+ if any(not isinstance(value, str) or value not in RESULTS for value in checks.values()):
66
+ raise AuditError("Invalid check result")
67
+ if type(record.get("permissionsConfirmed")) is not bool:
68
+ raise AuditError("permissionsConfirmed must be a boolean")
69
+ failed = tuple(name for name in CHECKS if checks[name] == "fail")
70
+ pending = tuple(name for name in CHECKS if checks[name] == "pending")
71
+ if failed:
72
+ status, reason = "revise", "failed_checks"
73
+ elif pending:
74
+ status, reason = "pending", "incomplete_checks"
75
+ elif not any(value == "pass" for value in checks.values()):
76
+ status, reason = "pending", "no_positive_check"
77
+ elif not record["permissionsConfirmed"]:
78
+ status, reason = "pending", "permissions_unconfirmed"
79
+ else:
80
+ status, reason = "accepted", "manual_checks_complete"
81
+ return AuditResult(record_id, candidate, status, reason, quote, failed, pending)
82
+
83
+
84
+ def audit_records(records: Any) -> dict[str, Any]:
85
+ """Audit up to 10,000 records, reject duplicate ids, and total all known quotes."""
86
+ if not isinstance(records, list) or len(records) > 10000:
87
+ raise AuditError("Expected a JSON array containing at most 10000 records")
88
+ ids: set[str] = set()
89
+ outcomes = []
90
+ decisions = {"pending": 0, "revise": 0, "accepted": 0}
91
+ values, unknown = [], 0
92
+ for index, record in enumerate(records):
93
+ try:
94
+ result = audit_record(record)
95
+ if result.record_id in ids:
96
+ raise AuditError(f"Duplicate record id: {result.record_id}")
97
+ except AuditError as error:
98
+ raise AuditError(f"Record {index}: {error}") from error
99
+ ids.add(result.record_id)
100
+ decisions[result.status] += 1
101
+ if result.quoted_credits is None:
102
+ unknown += 1
103
+ else:
104
+ values.append(result.quoted_credits)
105
+ outcomes.append({"id": result.record_id, "candidate": result.candidate, "status": result.status,
106
+ "reason": result.reason, "quotedCredits": None if result.quoted_credits is None else _amount(result.quoted_credits),
107
+ "failed": list(result.failed), "pending": list(result.pending)})
108
+ with localcontext() as context:
109
+ context.prec = 28
110
+ total = sum(values, Decimal("0"))
111
+ return {"schema": "civon-image-edit-audit/v1", "records": len(records), "quotedCredits": _amount(total),
112
+ "unknownQuotes": unknown, "decisions": decisions, "quoteMeaning": "sum_of_manually_entered_quotes_not_charges",
113
+ "results": outcomes}
114
+
115
+
116
+ def _pairs(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
117
+ result = {}
118
+ for key, value in pairs:
119
+ if key in result:
120
+ raise AuditError(f"Duplicate JSON key: {key}")
121
+ result[key] = value
122
+ return result
123
+
124
+
125
+ def _constant(value: str) -> None:
126
+ raise AuditError(f"Non-finite JSON constant: {value}")
127
+
128
+
129
+ def load_records(text: str) -> list[dict[str, Any]]:
130
+ """Decode strict JSON and validate before returning the records."""
131
+ try:
132
+ records = json.loads(text, object_pairs_hook=_pairs, parse_constant=_constant)
133
+ except (json.JSONDecodeError, RecursionError) as error:
134
+ raise AuditError(f"Invalid JSON: {error}") from error
135
+ audit_records(records)
136
+ return records
137
+
138
+
139
+ def to_markdown(records: Any) -> str:
140
+ """Render a compact table, escaping table pipes and line breaks, not HTML."""
141
+ report = audit_records(records)
142
+ def cell(value: Any) -> str:
143
+ return str(value).replace("|", "\\|").replace("\r", " ").replace("\n", " ")
144
+ lines = ["# Manual image-edit audit", "", "Human assertions and quoted credits; not image analysis, billing or rights verification.", "",
145
+ "| Record | Candidate | Quote | Decision |", "| --- | --- | --- | --- |"]
146
+ for row in report["results"]:
147
+ quote = "Unknown" if row["quotedCredits"] is None else row["quotedCredits"]
148
+ lines.append("| " + " | ".join(cell(v) for v in (row["id"], row["candidate"], quote, row["status"])) + " |")
149
+ lines += ["", f"Total entered quotes: {report['quotedCredits']}; unknown quotes: {report['unknownQuotes']}.", ""]
150
+ return "\n".join(lines)
@@ -0,0 +1,30 @@
1
+ """Read one local JSON array or stdin; write an audit to stdout."""
2
+ import argparse
3
+ import json
4
+ import sys
5
+ from .audit import AuditError, audit_records, load_records, to_markdown
6
+
7
+
8
+ def main(argv: list[str] | None = None) -> int:
9
+ parser = argparse.ArgumentParser(description="Audit human image-edit records; no image analysis or network access.")
10
+ parser.add_argument("file", help="local JSON file, or - for stdin")
11
+ parser.add_argument("--markdown", action="store_true", help="write a compact Markdown table")
12
+ args = parser.parse_args(argv)
13
+ try:
14
+ if args.file == "-":
15
+ data = sys.stdin.buffer.read(5 * 1024 * 1024 + 1)
16
+ else:
17
+ with open(args.file, "rb") as stream:
18
+ data = stream.read(5 * 1024 * 1024 + 1)
19
+ if len(data) > 5 * 1024 * 1024:
20
+ raise AuditError("Input exceeds 5 MiB")
21
+ records = load_records(data.decode("utf-8"))
22
+ print(to_markdown(records) if args.markdown else json.dumps(audit_records(records), indent=2, ensure_ascii=False))
23
+ return 0
24
+ except (AuditError, OSError, UnicodeError, RecursionError) as error:
25
+ print(f"Invalid edit records: {error}", file=sys.stderr)
26
+ return 1
27
+
28
+
29
+ if __name__ == "__main__":
30
+ raise SystemExit(main())
@@ -0,0 +1,167 @@
1
+ import copy
2
+ from dataclasses import FrozenInstanceError
3
+ from decimal import Decimal, localcontext
4
+ import json
5
+ import subprocess
6
+ import sys
7
+ import unittest
8
+ from unittest.mock import patch
9
+ from civon_image_edit_audit import AuditError, audit_record, audit_records, load_records, to_markdown
10
+
11
+
12
+ def record(identifier="a", quote=None):
13
+ return {"schema": "civon-image-edit-record/v1", "id": identifier, "candidate": "mug.png",
14
+ "prompt": "A warmer backdrop", "settings": "Manual settings", "quotedCredits": quote,
15
+ "checks": dict.fromkeys(("subject", "text", "geometry", "edges", "lighting", "delivery"), "pending"),
16
+ "permissionsConfirmed": False}
17
+
18
+
19
+ class AuditTests(unittest.TestCase):
20
+ def test_pending_and_immutable(self):
21
+ r = record()
22
+ before = copy.deepcopy(r)
23
+ result = audit_record(r)
24
+ self.assertEqual(result.status, "pending")
25
+ self.assertEqual(len(result.pending), 6)
26
+ self.assertEqual(r, before)
27
+ with self.assertRaises(FrozenInstanceError):
28
+ result.status = "accepted"
29
+
30
+ def test_permissions_gate_and_acceptance(self):
31
+ r = record()
32
+ r["checks"] = dict.fromkeys(r["checks"], "pass")
33
+ self.assertEqual(audit_record(r).reason, "permissions_unconfirmed")
34
+ r["permissionsConfirmed"] = True
35
+ self.assertEqual(audit_record(r).status, "accepted")
36
+
37
+ def test_failure_precedes_pending(self):
38
+ r = record()
39
+ r["checks"]["text"] = "fail"
40
+ self.assertEqual(audit_record(r).status, "revise")
41
+ self.assertEqual(audit_record(r).failed, ("text",))
42
+
43
+ def test_na_is_not_positive_evidence(self):
44
+ r = record()
45
+ r["checks"] = dict.fromkeys(r["checks"], "na")
46
+ r["permissionsConfirmed"] = True
47
+ self.assertEqual(audit_record(r).reason, "no_positive_check")
48
+ r["checks"]["subject"] = "pass"
49
+ self.assertEqual(audit_record(r).status, "accepted")
50
+
51
+ def test_quote_precision_and_unknown(self):
52
+ data = [record("a", "0.1"), record("b", "0.2"), record("c", None), record("d", "0")]
53
+ report = audit_records(data)
54
+ self.assertEqual(report["quotedCredits"], "0.3")
55
+ self.assertEqual(report["unknownQuotes"], 1)
56
+ self.assertEqual(audit_record(record(quote="999999.999")).quoted_credits, Decimal("999999.999"))
57
+ with localcontext() as context:
58
+ context.prec = 2
59
+ self.assertEqual(audit_records([record("a", "999999.999"), record("b", "0.001")])["quotedCredits"], "1000000")
60
+
61
+ def test_quotes_reject_non_contract_values(self):
62
+ for quote in [-1, 0, 1.2, True, "-1", "NaN", "Infinity", "1e3", "1.0001", "1000000", "", "١"]:
63
+ with self.subTest(quote=quote), self.assertRaises(AuditError):
64
+ audit_record(record(quote=quote))
65
+
66
+ def test_record_shape(self):
67
+ for value in [None, [], {}, {"schema": "other"}]:
68
+ with self.subTest(value=value), self.assertRaises(AuditError):
69
+ audit_record(value)
70
+
71
+ def test_required_strings(self):
72
+ for field in ["id", "candidate", "prompt", "settings"]:
73
+ for value in ["", " ", None, 2, True]:
74
+ with self.subTest(field=field, value=value), self.assertRaises(AuditError):
75
+ r = record()
76
+ r[field] = value
77
+ audit_record(r)
78
+ for field, limit in [("id", 160), ("candidate", 250), ("prompt", 2000), ("settings", 2000)]:
79
+ r = record()
80
+ r[field] = "x" * (limit + 1)
81
+ with self.assertRaises(AuditError):
82
+ audit_record(r)
83
+
84
+ def test_checks_exact_shape(self):
85
+ for change in ["missing", "extra", "invalid", "list", "unhashable"]:
86
+ r = record()
87
+ if change == "missing": del r["checks"]["text"]
88
+ if change == "extra": r["checks"]["invented"] = "pass"
89
+ if change == "invalid": r["checks"]["text"] = "yes"
90
+ if change == "list": r["checks"] = []
91
+ if change == "unhashable": r["checks"]["text"] = []
92
+ with self.subTest(change=change), self.assertRaises(AuditError):
93
+ audit_record(r)
94
+
95
+ def test_permission_boolean(self):
96
+ for value in [1, 0, "true", None, []]:
97
+ r = record()
98
+ r["permissionsConfirmed"] = value
99
+ with self.subTest(value=value), self.assertRaises(AuditError):
100
+ audit_record(r)
101
+
102
+ def test_checksums_and_notes(self):
103
+ r = record()
104
+ r["sourceSha256"] = "AB" * 32
105
+ r["candidateSha256"] = None
106
+ r["notes"] = "Private observation"
107
+ audit_record(r)
108
+ for field, value in [("sourceSha256", "bad"), ("candidateSha256", 3), ("notes", ""), ("notes", "x" * 2001)]:
109
+ bad = {**r, field: value}
110
+ with self.subTest(field=field), self.assertRaises(AuditError):
111
+ audit_record(bad)
112
+
113
+ def test_duplicates_and_error_index(self):
114
+ with self.assertRaisesRegex(AuditError, "Record 1: Duplicate"):
115
+ audit_records([record("a"), record(" a ")])
116
+ with self.assertRaisesRegex(AuditError, "Record 1:"):
117
+ audit_records([record(), {}])
118
+
119
+ def test_array_and_size(self):
120
+ self.assertEqual(audit_records([])["quotedCredits"], "0")
121
+ for data in [{}, None, [record()] * 10001]:
122
+ with self.subTest(kind=type(data).__name__), self.assertRaises(AuditError):
123
+ audit_records(data)
124
+
125
+ def test_json_strictness(self):
126
+ self.assertEqual(load_records(json.dumps([record()])), [record()])
127
+ for text in ['{"a":1,"a":2}', '[NaN]', '[Infinity]', '[', '[[]]', '[' * 2000 + ']' * 2000]:
128
+ with self.subTest(text=text[:20]), self.assertRaises(AuditError):
129
+ load_records(text)
130
+
131
+ def test_markdown_escape(self):
132
+ r = record("a|b")
133
+ r["candidate"] = "one\ntwo"
134
+ result = to_markdown([r])
135
+ self.assertIn("a\\|b", result)
136
+ self.assertIn("one two", result)
137
+ self.assertIn("Unknown", result)
138
+
139
+ def test_no_file_or_network_io(self):
140
+ with patch("builtins.open", side_effect=AssertionError("file I/O")), patch("socket.socket", side_effect=AssertionError("network")):
141
+ self.assertEqual(audit_records([record()])["records"], 1)
142
+
143
+ def test_summary_counts_failed_quotes(self):
144
+ r = record("failed", "2.50")
145
+ r["checks"]["text"] = "fail"
146
+ report = audit_records([r, record("pending", "1.25")])
147
+ self.assertEqual(report["quotedCredits"], "3.75")
148
+ self.assertEqual(report["decisions"], {"pending": 1, "revise": 1, "accepted": 0})
149
+
150
+ def test_cli(self):
151
+ def cli(args, data=b""):
152
+ return subprocess.run([sys.executable, "-m", "civon_image_edit_audit.cli", *args], input=data, capture_output=True)
153
+ data = json.dumps([record(quote="0.2")]).encode()
154
+ result = cli(["-"], data)
155
+ self.assertEqual(result.returncode, 0)
156
+ self.assertEqual(json.loads(result.stdout)["quotedCredits"], "0.2")
157
+ self.assertEqual(cli(["--markdown", "-"], data).returncode, 0)
158
+ self.assertEqual(cli(["--help"]).returncode, 0)
159
+ self.assertEqual(cli([]).returncode, 2)
160
+ self.assertEqual(cli(["-"], b"{bad").returncode, 1)
161
+ self.assertEqual(cli(["-"], b"\xff").returncode, 1)
162
+ self.assertEqual(cli(["-"], b" " * (5 * 1024 * 1024 + 1)).returncode, 1)
163
+ self.assertEqual(cli(["/nonexistent/civon-audit-test.json"]).returncode, 1)
164
+
165
+
166
+ if __name__ == "__main__":
167
+ unittest.main()