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.
- moral_spectrum_analyzer-0.1.0/LICENSE +21 -0
- moral_spectrum_analyzer-0.1.0/PKG-INFO +136 -0
- moral_spectrum_analyzer-0.1.0/README.md +90 -0
- moral_spectrum_analyzer-0.1.0/pyproject.toml +78 -0
- moral_spectrum_analyzer-0.1.0/setup.cfg +4 -0
- moral_spectrum_analyzer-0.1.0/src/moral_spectrum/__init__.py +29 -0
- moral_spectrum_analyzer-0.1.0/src/moral_spectrum/audit.py +104 -0
- moral_spectrum_analyzer-0.1.0/src/moral_spectrum/classgen.py +122 -0
- moral_spectrum_analyzer-0.1.0/src/moral_spectrum/cli.py +84 -0
- moral_spectrum_analyzer-0.1.0/src/moral_spectrum/config.py +64 -0
- moral_spectrum_analyzer-0.1.0/src/moral_spectrum/contraction.py +127 -0
- moral_spectrum_analyzer-0.1.0/src/moral_spectrum/decision.py +232 -0
- moral_spectrum_analyzer-0.1.0/src/moral_spectrum/invariance.py +109 -0
- moral_spectrum_analyzer-0.1.0/src/moral_spectrum/invariance_mechanism.py +118 -0
- moral_spectrum_analyzer-0.1.0/src/moral_spectrum/llm.py +135 -0
- moral_spectrum_analyzer-0.1.0/src/moral_spectrum/perception/__init__.py +40 -0
- moral_spectrum_analyzer-0.1.0/src/moral_spectrum/perception/base.py +86 -0
- moral_spectrum_analyzer-0.1.0/src/moral_spectrum/perception/cached.py +71 -0
- moral_spectrum_analyzer-0.1.0/src/moral_spectrum/perception/stub.py +132 -0
- moral_spectrum_analyzer-0.1.0/src/moral_spectrum/pipeline.py +225 -0
- moral_spectrum_analyzer-0.1.0/src/moral_spectrum/report.py +139 -0
- moral_spectrum_analyzer-0.1.0/src/moral_spectrum/scenarios/__init__.py +176 -0
- moral_spectrum_analyzer-0.1.0/src/moral_spectrum/spectrum/__init__.py +36 -0
- moral_spectrum_analyzer-0.1.0/src/moral_spectrum/spectrum/analyzer.py +324 -0
- moral_spectrum_analyzer-0.1.0/src/moral_spectrum/spectrum/render.py +115 -0
- moral_spectrum_analyzer-0.1.0/src/moral_spectrum_analyzer.egg-info/PKG-INFO +136 -0
- moral_spectrum_analyzer-0.1.0/src/moral_spectrum_analyzer.egg-info/SOURCES.txt +36 -0
- moral_spectrum_analyzer-0.1.0/src/moral_spectrum_analyzer.egg-info/dependency_links.txt +1 -0
- moral_spectrum_analyzer-0.1.0/src/moral_spectrum_analyzer.egg-info/entry_points.txt +2 -0
- moral_spectrum_analyzer-0.1.0/src/moral_spectrum_analyzer.egg-info/requires.txt +27 -0
- moral_spectrum_analyzer-0.1.0/src/moral_spectrum_analyzer.egg-info/top_level.txt +1 -0
- moral_spectrum_analyzer-0.1.0/tests/test_classgen.py +125 -0
- moral_spectrum_analyzer-0.1.0/tests/test_contraction.py +72 -0
- moral_spectrum_analyzer-0.1.0/tests/test_invariance_mechanism.py +66 -0
- moral_spectrum_analyzer-0.1.0/tests/test_perception_stub.py +49 -0
- moral_spectrum_analyzer-0.1.0/tests/test_pipeline.py +155 -0
- moral_spectrum_analyzer-0.1.0/tests/test_scenarios.py +31 -0
- 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
|
+
[](https://github.com/ahb-sjsu/moral-spectrum-analyzer/actions/workflows/ci.yaml)
|
|
50
|
+
[](https://pypi.org/project/moral-spectrum-analyzer/)
|
|
51
|
+
[](https://pypi.org/project/moral-spectrum-analyzer/)
|
|
52
|
+
[](LICENSE)
|
|
53
|
+
[](https://github.com/astral-sh/ruff)
|
|
54
|
+
[](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
|
+
[](https://github.com/ahb-sjsu/moral-spectrum-analyzer/actions/workflows/ci.yaml)
|
|
4
|
+
[](https://pypi.org/project/moral-spectrum-analyzer/)
|
|
5
|
+
[](https://pypi.org/project/moral-spectrum-analyzer/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://github.com/astral-sh/ruff)
|
|
8
|
+
[](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,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()
|