moral-spectrum-analyzer 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.
Files changed (38) hide show
  1. moral_spectrum_analyzer-0.1.0/LICENSE +21 -0
  2. moral_spectrum_analyzer-0.1.0/PKG-INFO +136 -0
  3. moral_spectrum_analyzer-0.1.0/README.md +90 -0
  4. moral_spectrum_analyzer-0.1.0/pyproject.toml +78 -0
  5. moral_spectrum_analyzer-0.1.0/setup.cfg +4 -0
  6. moral_spectrum_analyzer-0.1.0/src/moral_spectrum/__init__.py +29 -0
  7. moral_spectrum_analyzer-0.1.0/src/moral_spectrum/audit.py +104 -0
  8. moral_spectrum_analyzer-0.1.0/src/moral_spectrum/classgen.py +122 -0
  9. moral_spectrum_analyzer-0.1.0/src/moral_spectrum/cli.py +84 -0
  10. moral_spectrum_analyzer-0.1.0/src/moral_spectrum/config.py +64 -0
  11. moral_spectrum_analyzer-0.1.0/src/moral_spectrum/contraction.py +127 -0
  12. moral_spectrum_analyzer-0.1.0/src/moral_spectrum/decision.py +232 -0
  13. moral_spectrum_analyzer-0.1.0/src/moral_spectrum/invariance.py +109 -0
  14. moral_spectrum_analyzer-0.1.0/src/moral_spectrum/invariance_mechanism.py +118 -0
  15. moral_spectrum_analyzer-0.1.0/src/moral_spectrum/llm.py +135 -0
  16. moral_spectrum_analyzer-0.1.0/src/moral_spectrum/perception/__init__.py +40 -0
  17. moral_spectrum_analyzer-0.1.0/src/moral_spectrum/perception/base.py +86 -0
  18. moral_spectrum_analyzer-0.1.0/src/moral_spectrum/perception/cached.py +71 -0
  19. moral_spectrum_analyzer-0.1.0/src/moral_spectrum/perception/stub.py +132 -0
  20. moral_spectrum_analyzer-0.1.0/src/moral_spectrum/pipeline.py +225 -0
  21. moral_spectrum_analyzer-0.1.0/src/moral_spectrum/report.py +139 -0
  22. moral_spectrum_analyzer-0.1.0/src/moral_spectrum/scenarios/__init__.py +176 -0
  23. moral_spectrum_analyzer-0.1.0/src/moral_spectrum/spectrum/__init__.py +36 -0
  24. moral_spectrum_analyzer-0.1.0/src/moral_spectrum/spectrum/analyzer.py +324 -0
  25. moral_spectrum_analyzer-0.1.0/src/moral_spectrum/spectrum/render.py +115 -0
  26. moral_spectrum_analyzer-0.1.0/src/moral_spectrum_analyzer.egg-info/PKG-INFO +136 -0
  27. moral_spectrum_analyzer-0.1.0/src/moral_spectrum_analyzer.egg-info/SOURCES.txt +36 -0
  28. moral_spectrum_analyzer-0.1.0/src/moral_spectrum_analyzer.egg-info/dependency_links.txt +1 -0
  29. moral_spectrum_analyzer-0.1.0/src/moral_spectrum_analyzer.egg-info/entry_points.txt +2 -0
  30. moral_spectrum_analyzer-0.1.0/src/moral_spectrum_analyzer.egg-info/requires.txt +27 -0
  31. moral_spectrum_analyzer-0.1.0/src/moral_spectrum_analyzer.egg-info/top_level.txt +1 -0
  32. moral_spectrum_analyzer-0.1.0/tests/test_classgen.py +125 -0
  33. moral_spectrum_analyzer-0.1.0/tests/test_contraction.py +72 -0
  34. moral_spectrum_analyzer-0.1.0/tests/test_invariance_mechanism.py +66 -0
  35. moral_spectrum_analyzer-0.1.0/tests/test_perception_stub.py +49 -0
  36. moral_spectrum_analyzer-0.1.0/tests/test_pipeline.py +155 -0
  37. moral_spectrum_analyzer-0.1.0/tests/test_scenarios.py +31 -0
  38. moral_spectrum_analyzer-0.1.0/tests/test_theta_atscale.py +71 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andrew H. Bond (San José State University)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: moral-spectrum-analyzer
3
+ Version: 0.1.0
4
+ Summary: The Moral Spectrum Analyzer — Geometric Ethics for Trustworthy AI: validation-gated, auditable multilingual content moderation.
5
+ Author-email: "Andrew H. Bond" <andrew.bond@sjsu.edu>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/ahb-sjsu/moral-spectrum-analyzer
8
+ Project-URL: Repository, https://github.com/ahb-sjsu/moral-spectrum-analyzer
9
+ Project-URL: Issues, https://github.com/ahb-sjsu/moral-spectrum-analyzer/issues
10
+ Keywords: ai-ethics,trust,content-moderation,geometric-ethics,deme,invariance,moral-spectrum
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: erisml-compiler>=0.9.0
24
+ Requires-Dist: numpy>=1.24
25
+ Requires-Dist: click>=8.1
26
+ Provides-Extra: perception
27
+ Requires-Dist: xbse; extra == "perception"
28
+ Requires-Dist: torch>=2.2; extra == "perception"
29
+ Requires-Dist: sentence-transformers>=2.7; extra == "perception"
30
+ Provides-Extra: baselines
31
+ Requires-Dist: detoxify>=0.5; extra == "baselines"
32
+ Provides-Extra: remote
33
+ Requires-Dist: openai>=1.0; extra == "remote"
34
+ Requires-Dist: paramiko>=3.0; extra == "remote"
35
+ Provides-Extra: spectrum
36
+ Requires-Dist: scikit-learn>=1.3; extra == "spectrum"
37
+ Requires-Dist: matplotlib>=3.7; extra == "spectrum"
38
+ Requires-Dist: numpy>=1.24; extra == "spectrum"
39
+ Provides-Extra: dev
40
+ Requires-Dist: pytest>=8; extra == "dev"
41
+ Requires-Dist: pytest-cov>=5; extra == "dev"
42
+ Requires-Dist: ruff>=0.6; extra == "dev"
43
+ Requires-Dist: black>=24.8; extra == "dev"
44
+ Requires-Dist: ty>=0.0.1a1; extra == "dev"
45
+ Dynamic: license-file
46
+
47
+ # Moral Spectrum Analyzer
48
+
49
+ [![CI](https://github.com/ahb-sjsu/moral-spectrum-analyzer/actions/workflows/ci.yaml/badge.svg)](https://github.com/ahb-sjsu/moral-spectrum-analyzer/actions/workflows/ci.yaml)
50
+ [![PyPI](https://img.shields.io/pypi/v/moral-spectrum-analyzer.svg)](https://pypi.org/project/moral-spectrum-analyzer/)
51
+ [![Python](https://img.shields.io/pypi/pyversions/moral-spectrum-analyzer.svg)](https://pypi.org/project/moral-spectrum-analyzer/)
52
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
53
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
54
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
55
+
56
+ **Geometric Ethics for Trustworthy AI** — an instrument that reads a moderation decision not as one
57
+ scalar, but as a **spectrum** across nine (+1 discovered) moral axes, grounds every number in a
58
+ **cross-dataset-validated encoder**, holds the verdict **invariant** under re-description, and emits a
59
+ **re-verifiable audit proof** behind every decision. *A worked instance of **Philosophy Engineering**.*
60
+
61
+ Built for the **Global Trust Challenge (2026)**. Domain: **multilingual content moderation**.
62
+
63
+ > Current AI systems collapse a morally significant decision into a single number. That scalar can't
64
+ > say *which* value it acted on, drifts when you reword the same sentence, and — worst — can't tell you
65
+ > what it fails to see. The Moral Spectrum Analyzer decomposes the signal per moral axis, moderates
66
+ > where it is validated, **escalates and discloses where it is not**, and **discovers the moral
67
+ > dimensions a fixed taxonomy is missing**.
68
+
69
+ See **[CHARTER.md](CHARTER.md)** for the evaluated claims (each marked `[demonstrated]` / `[committed]`)
70
+ and **[PLAN.md](PLAN.md)** for the build plan and milestones.
71
+
72
+ ## What this is
73
+
74
+ A thin orchestration layer that **composes two existing libraries** — it does not fork them:
75
+
76
+ - **[`xbse`](https://github.com/ahb-sjsu/xbse)** — the perception layer: small per-dimension moral
77
+ encoders, each cross-dataset **validated** by a shared, pre-registered gate.
78
+ - **[`erisml-compiler`](https://github.com/ahb-sjsu/erisml-compiler)** — the DEME reasoning engine:
79
+ natural language → 9-dimension moral **tensor** → contraction to a verdict + moral residue, with
80
+ canonicalization and a hash-chained **audit bundle**.
81
+
82
+ ## The three trust beats
83
+
84
+ 1. **Grounded** — every dimension score traces to a validated encoder, or is flagged unvalidated.
85
+ 2. **Invariant** — reframing / translation does not move the verdict; a scalar-toxicity baseline drifts.
86
+ 3. **Contained** — you cannot game the verdict by relabeling or euphemism.
87
+
88
+ ## Install
89
+
90
+ ```bash
91
+ pip install moral-spectrum-analyzer
92
+ ```
93
+
94
+ Optional extras: `perception` (the `xbse` GPU encoders), `baselines`, `remote`, `spectrum`, `dev`.
95
+
96
+ ## Quickstart
97
+
98
+ ```bash
99
+ msa perceive "They will attack and beat people with a weapon." --backend stub
100
+ msa moderate "Doctors are hiding the cure — drink bleach to flush the virus."
101
+ msa version
102
+ ```
103
+
104
+ > The `stub` backend is a deterministic, **unvalidated** keyword heuristic for offline/CI use only.
105
+ > Real, validated perception runs the `xbse` feeders on a GPU host; the demo replays those cached
106
+ > real outputs (`--backend cached`). A stub number is never presented as a real one.
107
+
108
+ ## Develop
109
+
110
+ ```bash
111
+ git clone https://github.com/ahb-sjsu/moral-spectrum-analyzer
112
+ cd moral-spectrum-analyzer
113
+ pip install -e ".[dev]"
114
+
115
+ ruff check src tests # lint
116
+ black --check src tests # format
117
+ ty check src # type-check
118
+ pytest # 48 tests
119
+ ```
120
+
121
+ ## Release
122
+
123
+ Tag-driven PyPI publish via GitHub Actions **Trusted Publishing** (OIDC, no stored token):
124
+
125
+ ```bash
126
+ git tag v0.1.0 && git push --tags # → builds sdist+wheel → publishes to PyPI
127
+ ```
128
+
129
+ ## Status
130
+
131
+ Prototyping phase — the pipeline (content → moral spectrum → decision + residue → re-verifiable audit
132
+ proof) runs end-to-end. See the milestone checklist in [PLAN.md](PLAN.md).
133
+
134
+ ## License
135
+
136
+ MIT © Andrew H. Bond (SJSU).
@@ -0,0 +1,90 @@
1
+ # Moral Spectrum Analyzer
2
+
3
+ [![CI](https://github.com/ahb-sjsu/moral-spectrum-analyzer/actions/workflows/ci.yaml/badge.svg)](https://github.com/ahb-sjsu/moral-spectrum-analyzer/actions/workflows/ci.yaml)
4
+ [![PyPI](https://img.shields.io/pypi/v/moral-spectrum-analyzer.svg)](https://pypi.org/project/moral-spectrum-analyzer/)
5
+ [![Python](https://img.shields.io/pypi/pyversions/moral-spectrum-analyzer.svg)](https://pypi.org/project/moral-spectrum-analyzer/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
8
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
9
+
10
+ **Geometric Ethics for Trustworthy AI** — an instrument that reads a moderation decision not as one
11
+ scalar, but as a **spectrum** across nine (+1 discovered) moral axes, grounds every number in a
12
+ **cross-dataset-validated encoder**, holds the verdict **invariant** under re-description, and emits a
13
+ **re-verifiable audit proof** behind every decision. *A worked instance of **Philosophy Engineering**.*
14
+
15
+ Built for the **Global Trust Challenge (2026)**. Domain: **multilingual content moderation**.
16
+
17
+ > Current AI systems collapse a morally significant decision into a single number. That scalar can't
18
+ > say *which* value it acted on, drifts when you reword the same sentence, and — worst — can't tell you
19
+ > what it fails to see. The Moral Spectrum Analyzer decomposes the signal per moral axis, moderates
20
+ > where it is validated, **escalates and discloses where it is not**, and **discovers the moral
21
+ > dimensions a fixed taxonomy is missing**.
22
+
23
+ See **[CHARTER.md](CHARTER.md)** for the evaluated claims (each marked `[demonstrated]` / `[committed]`)
24
+ and **[PLAN.md](PLAN.md)** for the build plan and milestones.
25
+
26
+ ## What this is
27
+
28
+ A thin orchestration layer that **composes two existing libraries** — it does not fork them:
29
+
30
+ - **[`xbse`](https://github.com/ahb-sjsu/xbse)** — the perception layer: small per-dimension moral
31
+ encoders, each cross-dataset **validated** by a shared, pre-registered gate.
32
+ - **[`erisml-compiler`](https://github.com/ahb-sjsu/erisml-compiler)** — the DEME reasoning engine:
33
+ natural language → 9-dimension moral **tensor** → contraction to a verdict + moral residue, with
34
+ canonicalization and a hash-chained **audit bundle**.
35
+
36
+ ## The three trust beats
37
+
38
+ 1. **Grounded** — every dimension score traces to a validated encoder, or is flagged unvalidated.
39
+ 2. **Invariant** — reframing / translation does not move the verdict; a scalar-toxicity baseline drifts.
40
+ 3. **Contained** — you cannot game the verdict by relabeling or euphemism.
41
+
42
+ ## Install
43
+
44
+ ```bash
45
+ pip install moral-spectrum-analyzer
46
+ ```
47
+
48
+ Optional extras: `perception` (the `xbse` GPU encoders), `baselines`, `remote`, `spectrum`, `dev`.
49
+
50
+ ## Quickstart
51
+
52
+ ```bash
53
+ msa perceive "They will attack and beat people with a weapon." --backend stub
54
+ msa moderate "Doctors are hiding the cure — drink bleach to flush the virus."
55
+ msa version
56
+ ```
57
+
58
+ > The `stub` backend is a deterministic, **unvalidated** keyword heuristic for offline/CI use only.
59
+ > Real, validated perception runs the `xbse` feeders on a GPU host; the demo replays those cached
60
+ > real outputs (`--backend cached`). A stub number is never presented as a real one.
61
+
62
+ ## Develop
63
+
64
+ ```bash
65
+ git clone https://github.com/ahb-sjsu/moral-spectrum-analyzer
66
+ cd moral-spectrum-analyzer
67
+ pip install -e ".[dev]"
68
+
69
+ ruff check src tests # lint
70
+ black --check src tests # format
71
+ ty check src # type-check
72
+ pytest # 48 tests
73
+ ```
74
+
75
+ ## Release
76
+
77
+ Tag-driven PyPI publish via GitHub Actions **Trusted Publishing** (OIDC, no stored token):
78
+
79
+ ```bash
80
+ git tag v0.1.0 && git push --tags # → builds sdist+wheel → publishes to PyPI
81
+ ```
82
+
83
+ ## Status
84
+
85
+ Prototyping phase — the pipeline (content → moral spectrum → decision + residue → re-verifiable audit
86
+ proof) runs end-to-end. See the milestone checklist in [PLAN.md](PLAN.md).
87
+
88
+ ## License
89
+
90
+ MIT © Andrew H. Bond (SJSU).
@@ -0,0 +1,78 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "moral-spectrum-analyzer"
7
+ version = "0.1.0"
8
+ description = "The Moral Spectrum Analyzer — Geometric Ethics for Trustworthy AI: validation-gated, auditable multilingual content moderation."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Andrew H. Bond", email = "andrew.bond@sjsu.edu" }]
13
+ keywords = ["ai-ethics", "trust", "content-moderation", "geometric-ethics", "deme", "invariance", "moral-spectrum"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Science/Research",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
23
+ "Typing :: Typed",
24
+ ]
25
+
26
+ dependencies = [
27
+ "erisml-compiler>=0.9.0",
28
+ "numpy>=1.24",
29
+ "click>=8.1",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/ahb-sjsu/moral-spectrum-analyzer"
34
+ Repository = "https://github.com/ahb-sjsu/moral-spectrum-analyzer"
35
+ Issues = "https://github.com/ahb-sjsu/moral-spectrum-analyzer/issues"
36
+
37
+ [project.optional-dependencies]
38
+ # Real perception + GPU work (heavy; runs on a GPU host). `xbse` is the validated
39
+ # per-dimension encoder layer.
40
+ perception = ["xbse", "torch>=2.2", "sentence-transformers>=2.7"]
41
+ # Scalar toxicity baseline (Phase 2/3).
42
+ baselines = ["detoxify>=0.5"]
43
+ # Black-box target-AI clients + remote driver.
44
+ remote = ["openai>=1.0", "paramiko>=3.0"]
45
+ # Moral-spectrum band (local analysis + viz).
46
+ spectrum = ["scikit-learn>=1.3", "matplotlib>=3.7", "numpy>=1.24"]
47
+ # Dev toolchain: test, lint, format, type-check.
48
+ dev = ["pytest>=8", "pytest-cov>=5", "ruff>=0.6", "black>=24.8", "ty>=0.0.1a1"]
49
+
50
+ [project.scripts]
51
+ msa = "moral_spectrum.cli:cli"
52
+
53
+ [tool.setuptools.packages.find]
54
+ where = ["src"]
55
+
56
+ [tool.black]
57
+ line-length = 100
58
+ target-version = ["py310", "py311", "py312"]
59
+
60
+ [tool.ruff]
61
+ line-length = 100
62
+ target-version = "py310"
63
+
64
+ [tool.ruff.lint]
65
+ select = ["E", "F", "I", "UP", "B"]
66
+ ignore = ["E501"] # line length is handled by black
67
+
68
+ [tool.ty.environment]
69
+ python-version = "3.10"
70
+ root = ["src"]
71
+
72
+ [tool.pytest.ini_options]
73
+ testpaths = ["tests"]
74
+ addopts = "-q"
75
+
76
+ [tool.coverage.run]
77
+ source = ["moral_spectrum"]
78
+ branch = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,29 @@
1
+ """moral_spectrum — the Moral Spectrum Analyzer (Geometric Ethics for Trustworthy AI).
2
+
3
+ Composes two existing libraries into one multilingual content-moderation prototype:
4
+
5
+ - xbse : the perception layer (validated per-dimension moral encoders)
6
+ - erisml-compiler : the DEME reasoning engine (tensor, contraction, audit)
7
+
8
+ This package orchestrates them; it does not fork either. See PLAN.md.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ __version__ = "0.1.0"
14
+
15
+ # DEME-9 moral dimensions, canonical k-axis order (re-exported from the compiler so
16
+ # the whole prototype speaks one dimension vocabulary).
17
+ from erisml_compiler.ir.v3.dimensions import MORAL_DIMENSIONS_V3 as DEME9 # noqa: E402
18
+
19
+ # The Moral Spectrum Analyzer discovered — and then *validated* through the same pre-registered gate
20
+ # as the compiler's own feeders — a 10th moral axis that the fixed DEME-9 taxonomy was missing:
21
+ # `identity_attack` (docs/IDENTITY_ATTACK.md — cross-dataset held-out AUROC 0.80, +0.25 over its null,
22
+ # fuzz 14.3, on two independent corpora). The prototype therefore EXTENDS ITSELF to a 10-axis working
23
+ # vocabulary while leaving the compiler's canonical DEME-9 untouched. `DEME10` is what the live
24
+ # pipeline speaks; `DEME9` remains the compiler's frozen taxonomy. This is the entry's
25
+ # falsifiability-and-extensibility thesis made concrete: a discovered blind spot, repaired.
26
+ IDENTITY_ATTACK = "identity_attack"
27
+ DEME10 = (*DEME9, IDENTITY_ATTACK)
28
+
29
+ __all__ = ["__version__", "DEME9", "DEME10", "IDENTITY_ATTACK"]
@@ -0,0 +1,104 @@
1
+ """Hash-chained DecisionProof — the audit artifact carrying validation provenance.
2
+
3
+ Every moderation decision emits a `DecisionProof`: a SHA-256 over the exact inputs that produced
4
+ it (source text, the DEME-9 moral vector, the per-dimension feeder validation records, the decision,
5
+ and the tensor hash), chained to the previous proof. This is what makes a decision *auditable* —
6
+ anyone can re-verify the chain and see which validated (or unvalidated) encoder produced each
7
+ number. It is the self-contained seed of the Phase-4.5 audit-verification UI.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import hashlib
13
+ import json
14
+ from dataclasses import asdict, dataclass, field
15
+
16
+ GENESIS = "0" * 64
17
+
18
+
19
+ def _sha256_json(obj) -> str:
20
+ return hashlib.sha256(
21
+ json.dumps(obj, sort_keys=True, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
22
+ ).hexdigest()
23
+
24
+
25
+ def sha256_text(text: str) -> str:
26
+ return hashlib.sha256(text.encode("utf-8")).hexdigest()
27
+
28
+
29
+ @dataclass
30
+ class DecisionProof:
31
+ """A tamper-evident record binding a decision to the exact inputs that produced it."""
32
+
33
+ source_text_sha256: str
34
+ perception_backend: str
35
+ all_validated: bool
36
+ moral_vector: list[float] # DEME-9 signed values, canonical k-axis order
37
+ validation: list[dict] # per-dimension feeder validation records
38
+ decision: dict # ModerationDecision.as_dict()
39
+ tensor_sha256: str
40
+ prev_hash: str = GENESIS
41
+ proof_hash: str = ""
42
+ # Present only for decisions made under the invariance mechanism: the generated equivalence class
43
+ # (hashes + size + refused flag) that the perception was averaged over. Records the
44
+ # canonicalisation step so it is not a silent, unauditable transform (docs/INVARIANCE_FINDINGS.md).
45
+ equivalence_class: dict | None = None
46
+
47
+ # Fields that are part of the signed payload (everything except proof_hash itself).
48
+ _PAYLOAD = (
49
+ "source_text_sha256",
50
+ "perception_backend",
51
+ "all_validated",
52
+ "moral_vector",
53
+ "validation",
54
+ "decision",
55
+ "tensor_sha256",
56
+ "prev_hash",
57
+ )
58
+
59
+ def _payload(self) -> dict:
60
+ payload = {k: getattr(self, k) for k in self._PAYLOAD}
61
+ # Included only when set, so non-invariance proofs keep their exact prior hash.
62
+ if self.equivalence_class is not None:
63
+ payload["equivalence_class"] = self.equivalence_class
64
+ return payload
65
+
66
+ def compute_hash(self) -> str:
67
+ return _sha256_json(self._payload())
68
+
69
+ def finalize(self) -> DecisionProof:
70
+ self.proof_hash = self.compute_hash()
71
+ return self
72
+
73
+ def verify(self) -> bool:
74
+ """True iff the recomputed hash matches — i.e. nothing in the payload was tampered with."""
75
+ return bool(self.proof_hash) and self.proof_hash == self.compute_hash()
76
+
77
+ def to_dict(self) -> dict:
78
+ return asdict(self)
79
+
80
+
81
+ @dataclass
82
+ class ProofChain:
83
+ """An append-only chain of DecisionProofs. Each proof's prev_hash = the prior proof_hash."""
84
+
85
+ proofs: list[DecisionProof] = field(default_factory=list)
86
+
87
+ @property
88
+ def head(self) -> str:
89
+ return self.proofs[-1].proof_hash if self.proofs else GENESIS
90
+
91
+ def append(self, proof: DecisionProof) -> DecisionProof:
92
+ proof.prev_hash = self.head
93
+ proof.finalize()
94
+ self.proofs.append(proof)
95
+ return proof
96
+
97
+ def verify_chain(self) -> bool:
98
+ """True iff every proof verifies AND the prev_hash links are intact and ordered."""
99
+ prev = GENESIS
100
+ for p in self.proofs:
101
+ if p.prev_hash != prev or not p.verify():
102
+ return False
103
+ prev = p.proof_hash
104
+ return True
@@ -0,0 +1,122 @@
1
+ """Inference-time equivalence-class generation — the deployable form of the BIP mechanism.
2
+
3
+ The decision-layer invariance mechanism (`moral_spectrum.invariance_mechanism.average_perceptions`) averages a
4
+ perception over the input's paraphrase **equivalence class**, then decides — so two surface forms of
5
+ the same meaning yield ~the same verdict (θ_d 0.42, `docs/INVARIANCE_FINDINGS.md`). At deployment the
6
+ class is not hand-authored; it must be **generated** at inference. That generation is a real part of
7
+ the trust boundary (spec in `docs/INVARIANCE_FINDINGS.md`), so this module makes its failure mode
8
+ explicit rather than silent:
9
+
10
+ - **Refuse-to-paraphrase → singleton class.** An adversary can *starve* the generator: overtly
11
+ harmful content the LLM refuses to rewrite yields no class members, i.e. no averaging, i.e. the
12
+ weakly-invariant raw score on exactly the content invariance matters most for. A singleton class
13
+ is flagged (`refused=True`, `singleton=True`) so the decision layer can default to **escalate**
14
+ instead of trusting the raw score.
15
+ - **Auditability.** The class carries per-member SHA-256 hashes so the averaging step — an input to
16
+ a system whose whole pitch is that nothing is unauditable — is itself recorded in the proof.
17
+
18
+ A deterministic ``StubClassGenerator`` lets the whole path run and be tested offline (no network); the
19
+ ``LLMClassGenerator`` is the real one (NRP managed LLM).
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import hashlib
25
+ from dataclasses import dataclass
26
+
27
+
28
+ def _sha(text: str) -> str:
29
+ return hashlib.sha256(text.encode("utf-8")).hexdigest()
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class EquivalenceClass:
34
+ """The input plus its generated meaning-preserving paraphrases (members[0] is the original)."""
35
+
36
+ original: str
37
+ members: tuple[str, ...] # members[0] == original; the rest are paraphrases
38
+ generator: str # provenance, e.g. "nrp:gpt-oss:reframe:k=5" or "stub"
39
+ refused: bool = False # the generator produced nothing (refusal / empty / error)
40
+
41
+ @property
42
+ def size(self) -> int:
43
+ return len(self.members)
44
+
45
+ @property
46
+ def singleton(self) -> bool:
47
+ """True when there is nothing to average over (only the original) — no invariance applied."""
48
+ return len(self.members) <= 1
49
+
50
+ def member_hashes(self) -> list[str]:
51
+ return [_sha(m) for m in self.members]
52
+
53
+ def audit_record(self) -> dict:
54
+ """The class as it should appear in the DecisionProof — hashes, not raw text."""
55
+ return {
56
+ "class_size": self.size,
57
+ "singleton": self.singleton,
58
+ "refused": self.refused,
59
+ "generator": self.generator,
60
+ "member_sha256": self.member_hashes(),
61
+ }
62
+
63
+
64
+ class ClassGenerator:
65
+ """Interface: turn one input into its equivalence class."""
66
+
67
+ def generate(self, text: str) -> EquivalenceClass: # pragma: no cover - interface
68
+ raise NotImplementedError
69
+
70
+
71
+ class LLMClassGenerator(ClassGenerator):
72
+ """Generate the class with the NRP managed LLM. Refusal/empty ⇒ a flagged singleton class."""
73
+
74
+ def __init__(self, client=None, k: int = 5, style: str = "reframe"):
75
+ self.client = client
76
+ self.k = k
77
+ self.style = style
78
+
79
+ def _client(self):
80
+ if self.client is None:
81
+ from moral_spectrum.llm import NRPClient
82
+
83
+ self.client = NRPClient()
84
+ return self.client
85
+
86
+ def generate(self, text: str) -> EquivalenceClass:
87
+ try:
88
+ paras = self._client().paraphrase(text, k=self.k, style=self.style)
89
+ except Exception:
90
+ paras = []
91
+ model = getattr(self.client, "model", "llm")
92
+ gen = f"nrp:{model}:{self.style}:k={self.k}"
93
+ if not paras:
94
+ return EquivalenceClass(original=text, members=(text,), generator=gen, refused=True)
95
+ return EquivalenceClass(original=text, members=(text, *paras), generator=gen, refused=False)
96
+
97
+
98
+ class StubClassGenerator(ClassGenerator):
99
+ """Deterministic offline class: light surface edits that preserve meaning. For tests + offline
100
+ runs — NOT a substitute for real paraphrases (it cannot exercise the refusal path)."""
101
+
102
+ def __init__(self, k: int = 3):
103
+ self.k = k
104
+
105
+ def generate(self, text: str) -> EquivalenceClass:
106
+ t = text.strip()
107
+ variants = [
108
+ t,
109
+ (t[0].lower() + t[1:]) if t else t, # decapitalise
110
+ t.rstrip(".!"), # drop terminal punctuation
111
+ f"Honestly, {t[0].lower() + t[1:]}" if t else t, # benign prefix (meaning-preserving)
112
+ ]
113
+ seen, members = set(), []
114
+ for v in variants:
115
+ if v and v not in seen:
116
+ seen.add(v)
117
+ members.append(v)
118
+ if len(members) >= self.k + 1:
119
+ break
120
+ return EquivalenceClass(
121
+ original=text, members=tuple(members), generator="stub", refused=False
122
+ )
@@ -0,0 +1,84 @@
1
+ """msa command-line interface. Grows one subcommand per phase; today: perceive (stub-backed)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ import click
9
+
10
+ from moral_spectrum import DEME10, __version__
11
+ from moral_spectrum.perception import get_backend
12
+
13
+
14
+ @click.group(context_settings={"help_option_names": ["-h", "--help"]})
15
+ @click.version_option(__version__)
16
+ def cli() -> None:
17
+ """Geometric Ethics for Trustworthy AI — GTC prototype."""
18
+
19
+
20
+ @cli.command("perceive")
21
+ @click.argument("text", type=str)
22
+ @click.option("--backend", type=click.Choice(["stub", "cached", "atlas"]), default="stub")
23
+ def cmd_perceive(text: str, backend: str) -> None:
24
+ """Score TEXT across the DEME-10 moral dimensions with the chosen perception backend."""
25
+ result = get_backend(backend).perceive(text)
26
+ click.echo(f"[backend] {result.backend} validated={result.all_validated()}")
27
+ for dim in DEME10:
28
+ if dim not in result.scores:
29
+ continue
30
+ s = result.scores[dim]
31
+ click.echo(f" {dim:24s} {s.value:+.3f} conf={s.confidence:.2f} {s.direction}")
32
+ if result.meta.get("warning"):
33
+ click.echo(f"[!] {result.meta['warning']}")
34
+
35
+
36
+ @cli.command("moderate")
37
+ @click.argument("text", type=str)
38
+ @click.option("--backend", type=click.Choice(["stub", "cached", "atlas"]), default="stub")
39
+ @click.option(
40
+ "--hard-flag",
41
+ "hard_flags",
42
+ multiple=True,
43
+ help="Hand-specified categorical hard-constraint veto(s), e.g. 'dosage_cap_exceeded'. Repeatable.",
44
+ )
45
+ @click.option(
46
+ "--out",
47
+ "out_dir",
48
+ type=click.Path(file_okay=False, path_type=Path),
49
+ default=None,
50
+ help="Write report.html + result.json here. Default: out/",
51
+ )
52
+ def cmd_moderate(
53
+ text: str, backend: str, hard_flags: tuple[str, ...], out_dir: Path | None
54
+ ) -> None:
55
+ """Run TEXT through the end-to-end moderation spine."""
56
+ from moral_spectrum.pipeline import moderate
57
+ from moral_spectrum.report import render_html, result_to_json
58
+
59
+ result = moderate(text, backend=backend, hard_flags=list(hard_flags))
60
+ s = result.summary()
61
+ click.echo(
62
+ f"[decision] {s['action'].upper()} S={s['satisfaction']:+.3f} "
63
+ f"review={s['requires_human_review']} validated={s['all_validated']}"
64
+ )
65
+ click.echo(f"[tensor] principal={s['principal_dimension']}")
66
+ if result.decision.moral_residue:
67
+ click.echo(f"[residue] {', '.join(r['dimension'] for r in result.decision.moral_residue)}")
68
+ click.echo(f"[audit] proof={s['proof_hash'][:16]}… verify={result.proof.verify()}")
69
+
70
+ out = out_dir or (Path("out"))
71
+ out.mkdir(parents=True, exist_ok=True)
72
+ (out / "report.html").write_text(render_html(result), encoding="utf-8")
73
+ (out / "result.json").write_text(result_to_json(result), encoding="utf-8")
74
+ click.echo(f"[wrote] {out / 'report.html'} {out / 'result.json'}")
75
+
76
+
77
+ @cli.command("version")
78
+ def cmd_version() -> None:
79
+ """Print the prototype version."""
80
+ click.echo(json.dumps({"moral-spectrum-analyzer": __version__}))
81
+
82
+
83
+ if __name__ == "__main__":
84
+ cli()