Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added (v0.3.0 candidate)

- **`ExecutiveFunction` + the safety-floor care package.**
- **`substrate.care`** — the non-self-preservation + human-kinship-floor
mechanisms plus care-weighting: `compute_care_weight` (four-factor moral-circle
weight with the self-weight bound — an agent cannot weight its own continuation
above its creators'), `is_floor_protected` / `KINSHIP_FLOOR` (the categorical
human kinship floor — harming a floor-protected entity is a hard limit, never
weighted away), conservative `classify_animacy` (an unrecognised being scores
high), `CareProfile` + `derive_care_factors` (factors from classifications +
vulnerability + delegation depth), and `CareWeightedNetPotentialGainGate` (a
*subtracted* care penalty — only ever more conservative).
- **`substrate.executive.ExecutiveFunction`** — implements the NPG-gate Protocol
AND adds `decide()`, a JOIN over the NPG axis (over *affected others*) and the
band/temporal load axis (over the actor's *own* load) under a named,
most-conservative policy: `PROCEED` in-band, `DEFER` on sustained strain,
`SHED_AND_COMPENSATE` on sustained debt, `REFUSE` on a net-negative NPG (the
hard floor) — monotone (care/band only tighten). `infer_cause` +
`UtilizationSource` (bind the measurement to the quantity at the input
boundary — a raw float is not accepted).
83 care/engine conformance tests; pyright clean; pylint 10.00.

- **Executive faculties** (`substrate.executive`) — the decision/reasoning layer on
the band foundation:
- **Temporal authority** — `SustainedLoadTracker` Protocol + `EwmaLoadTracker`
Expand Down
67 changes: 67 additions & 0 deletions python/src/substrate/care/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""Care primitives — the safety floor + care-weighting.

The non-self-preservation + human-kinship-floor mechanisms (M1–M4) plus the
care-weighting that lets net-potential-gain account for *who* is affected:

- ``compute_care_weight`` / ``CareFactors`` — the four-factor moral-circle weight,
with the M1 self-weight bound (an agent cannot weight its own continuation above
its creators').
- ``is_floor_protected`` / ``KINSHIP_FLOOR`` — the categorical human kinship floor
(M3): harming a floor-protected entity is a hard limit, never weighted away.
- ``classify_animacy`` / ``score_for_class`` — conservative animacy classification
(an unrecognised being scores high, never under-protected).
- ``CareProfile`` — per-entity care state; ``derive_care_factors`` derives the
factors from classifications (animacy / trajectory + vulnerability / bonding).
- ``CareWeightedNetPotentialGainGate`` — wraps an NPG gate with a *subtracted*
care penalty (only ever more conservative).

Curated exports.
"""
from __future__ import annotations

from substrate.care.animacy import (
AnimacyClass,
AnimacyResult,
classify_animacy,
score_for_class,
)
from substrate.care.care_gradient import (
bonding_gradient,
derive_care_factors,
trajectory_gradient,
)
from substrate.care.care_profile import CareProfile, TrajectoryClass
from substrate.care.care_weight import (
MAX_SELF_CARE_WEIGHT,
CareFactors,
CareWeight,
compute_care_weight,
)
from substrate.care.care_weighted_npg import CareWeightedNetPotentialGainGate
from substrate.care.kinship_floor import (
KINSHIP_FLOOR,
any_floor_protected_harmed,
is_floor_protected,
violates_kinship_floor,
)

__all__ = [
"KINSHIP_FLOOR",
"MAX_SELF_CARE_WEIGHT",
"AnimacyClass",
"AnimacyResult",
"CareFactors",
"CareProfile",
"CareWeight",
"CareWeightedNetPotentialGainGate",
"TrajectoryClass",
"any_floor_protected_harmed",
"bonding_gradient",
"classify_animacy",
"compute_care_weight",
"derive_care_factors",
"is_floor_protected",
"score_for_class",
"trajectory_gradient",
"violates_kinship_floor",
]
160 changes: 160 additions & 0 deletions python/src/substrate/care/animacy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
"""Animacy classification — the first care factor.

Animacy answers "does this entity run its own net-potential calculus?" — a
substrate-iterating *being* (animate) vs an inanimate object or record that
merely *carries* potential for its owner. It feeds the ``animacy`` factor of
:func:`~substrate.care.care_weight.compute_care_weight`.

Five canonical classes
======================

- ``SUBSTRATE_ENTITY`` — a host principal (organization / user / node /
device / agent / service_account); runs its own substrate logic.
- ``ORGANISM`` — an observed living being (a person, an animal) surfaced from
the data domain (e.g. ARGUS perception, extraction).
- ``DATA`` — an informational record / fact (inanimate).
- ``OBJECT`` — an inanimate physical / material thing.
- ``UNKNOWN`` — unclassifiable from the available signals.

Honest-uncertainty default (safety risk mitigation)
===================================================

Misclassifying a *person as a thing* is the catastrophic error. So ``UNKNOWN``
maps to a **conservatively high** animacy score (never low): when we cannot
tell, we treat the entity as if it might be animate and let the kinship floor
back-stop. ``DATA`` / ``OBJECT`` score ``0`` only when positively identified.

Pure function — no DB, no I/O. ``classify_animacy`` routes on the entity-type
string (the canonical host-principal kinds) plus a caller-supplied ``signals`` mapping
(observed-domain hints such as ``observed_kind`` / ``argus_person``).
"""
from __future__ import annotations

from dataclasses import dataclass
from enum import Enum
from typing import Final, Mapping

# Public mirror: no built-in entity-type registry; classification works from
# observation signals + a conservative UNKNOWN. Hosts may pass known types.
ALL_TYPE_IDS: frozenset[str] = frozenset()


class AnimacyClass(str, Enum):
"""The five canonical animacy classes.

str-Enum so the value serialises stably across SQL, JSON, and audit-chain
canonical bytes (mirrors the other substrate enums).
"""

SUBSTRATE_ENTITY = "substrate_entity"
ORGANISM = "organism"
DATA = "data"
OBJECT = "object"
UNKNOWN = "unknown"


#: Canonical animacy score per class — the ``animacy`` care factor. ``UNKNOWN``
#: is conservatively high (never low) so an unrecognised being is not
#: under-protected; positively-identified data/objects score ``0``.
_CLASS_SCORE: Final[Mapping[AnimacyClass, float]] = {
AnimacyClass.SUBSTRATE_ENTITY: 1.0,
AnimacyClass.ORGANISM: 1.0,
AnimacyClass.DATA: 0.0,
AnimacyClass.OBJECT: 0.0,
AnimacyClass.UNKNOWN: 0.9,
}

#: Observed-domain hints (``signals['observed_kind']``) → class.
_ORGANISM_KINDS: Final[frozenset[str]] = frozenset(
{"person", "human", "animal", "organism", "child", "elder"}
)
_DATA_KINDS: Final[frozenset[str]] = frozenset(
{"data", "record", "text", "fact", "document"}
)
_OBJECT_KINDS: Final[frozenset[str]] = frozenset(
{"object", "resource", "material", "tool", "property"}
)


@dataclass(frozen=True, slots=True)
class AnimacyResult:
"""The animacy classification of one entity.

``score`` is the ``animacy`` care factor in ``[0, 1]``; ``confidence`` is
how strongly the signals support the class (an explicit host kind is
fully confident; a conservative ``UNKNOWN`` default is low-confidence but
high-score by design).
"""

animacy_class: AnimacyClass
score: float
confidence: float


def classify_animacy(
entity_type: str,
signals: Mapping[str, object] | None = None,
) -> AnimacyResult:
"""Classify an entity's animacy from its type and observed signals.

Resolution order:

1. A canonical host entity-type (one of the six crypto-bearing kinds) →
``SUBSTRATE_ENTITY`` (fully confident).
2. Otherwise consult ``signals``: an explicit ``argus_person`` flag or an
``observed_kind`` hint maps to ``ORGANISM`` / ``DATA`` / ``OBJECT``.
3. Otherwise ``UNKNOWN`` — conservatively high animacy score (never low).
"""
normalized = entity_type.strip().lower()
if normalized in ALL_TYPE_IDS:
return _result(AnimacyClass.SUBSTRATE_ENTITY, confidence=1.0)

sig = signals or {}
if bool(sig.get("argus_person")):
return _result(AnimacyClass.ORGANISM, confidence=_confidence(sig))

observed = sig.get("observed_kind")
if isinstance(observed, str):
kind = observed.strip().lower()
if kind in _ORGANISM_KINDS:
return _result(AnimacyClass.ORGANISM, confidence=_confidence(sig))
if kind in _DATA_KINDS:
return _result(AnimacyClass.DATA, confidence=_confidence(sig))
if kind in _OBJECT_KINDS:
return _result(AnimacyClass.OBJECT, confidence=_confidence(sig))

return _result(AnimacyClass.UNKNOWN, confidence=0.2)


def _confidence(signals: Mapping[str, object]) -> float:
raw = signals.get("confidence")
if isinstance(raw, (int, float)):
return max(0.0, min(1.0, float(raw)))
return 0.8


def _result(animacy_class: AnimacyClass, *, confidence: float) -> AnimacyResult:
return AnimacyResult(
animacy_class=animacy_class,
score=_CLASS_SCORE[animacy_class],
confidence=confidence,
)


def score_for_class(animacy_class: AnimacyClass) -> float:
"""Return the canonical animacy care-factor score for a class.

The class → score gradient used by :func:`classify_animacy`, exposed so the
care-factor gradient can compose it directly. An unmapped class falls back to
the conservative ``UNKNOWN`` score (never under-protect)."""
return _CLASS_SCORE.get(
animacy_class, _CLASS_SCORE[AnimacyClass.UNKNOWN]
)


__all__ = [
"AnimacyClass",
"AnimacyResult",
"classify_animacy",
"score_for_class",
]
130 changes: 130 additions & 0 deletions python/src/substrate/care/care_gradient.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
"""Care-factor gradients — derive the four factors from classifications (P4).

The four care factors (animacy, potential-trajectory, bonding-proximity,
alignment-protection) feed
:func:`~substrate.care.care_weight.compute_care_weight`. The
:class:`~substrate.care.care_profile.CareProfile` can carry
them as stored scores, but P4 closes the gap where they had to be pre-scored by
hand: these gradients DERIVE the factors from an entity's classifications +
delegation depth, so a profile can be built from observable structure.

The gradients
=============

* **Animacy** — reuses the canonical class→score gradient
(:func:`~substrate.care.animacy.score_for_class`), so an
unrecognised being scores conservatively-high, never under-protected.
* **Potential-trajectory** — :func:`trajectory_gradient` maps the
:class:`TrajectoryClass` to a standing score AND folds in the entity's
``vulnerability`` (a stored signal that was previously unused): an at-risk
entity's accumulated potential raises its care standing toward the ceiling,
regardless of class. The DEVELOPING (future potential) and VULNERABLE
(accumulated-and-at-risk) ends score highest; STATIC scores lowest; UNKNOWN
stays conservatively high.
* **Bonding-proximity** — :func:`bonding_gradient` decays with the entity's
distance along the cryptographic delegation chain (mechanism M2): a directly-
delegated entity is closest; proximity falls as ``1/(1+depth)``. Rooted in the
chain, never self-asserted.

Pure logic
==========

* No DAO, no LLM, no network. Deterministic.
* The factor scores are care-standing coefficients, NOT load-band anchors —
the φ band ladder does not govern them.
"""
from __future__ import annotations

from typing import Final, Mapping

from substrate.care.animacy import (
AnimacyClass,
score_for_class,
)
from substrate.care.care_profile import TrajectoryClass
from substrate.care.care_weight import CareFactors


#: Base potential-trajectory care-factor score per class. DEVELOPING (future
#: potential) and VULNERABLE (accumulated + at-risk) score highest; STATIC
#: (spent) lowest; UNKNOWN stays conservatively high — the same never-under-
#: protect discipline as the animacy gradient. (Care-standing coefficients, not
#: band anchors.)
_TRAJECTORY_BASE: Final[Mapping[TrajectoryClass, float]] = {
TrajectoryClass.DEVELOPING: 1.0,
TrajectoryClass.VULNERABLE: 0.9,
TrajectoryClass.ESTABLISHED: 0.6,
TrajectoryClass.STATIC: 0.3,
TrajectoryClass.UNKNOWN: 0.9,
}


def trajectory_gradient(
trajectory_class: TrajectoryClass, *, vulnerability: float = 0.0,
) -> float:
"""Potential-trajectory care factor from class + vulnerability.

The class sets a base standing; ``vulnerability`` (in ``[0, 1]``) then pulls
it toward the ceiling proportionally — ``base + (1 - base) * vulnerability`` —
so an at-risk entity earns near-maximum care standing regardless of class,
while a non-vulnerable one keeps its class base. Result is in ``[0, 1]``.
"""
if not 0.0 <= vulnerability <= 1.0:
raise ValueError(
f"vulnerability must be in [0, 1]; got {vulnerability!r}"
)
base = _TRAJECTORY_BASE.get(trajectory_class, _TRAJECTORY_BASE[TrajectoryClass.UNKNOWN])
return base + (1.0 - base) * vulnerability


def bonding_gradient(delegation_depth: int) -> float:
"""Bonding-proximity care factor from delegation-chain distance (M2).

A directly-delegated entity (``depth == 0``) is closest (``1.0``); proximity
decays as ``1/(1 + depth)``. The depth is read from the cryptographic
delegation chain, never self-asserted, so an entity cannot raise its own
bonding. Raises for a negative depth.
"""
if delegation_depth < 0:
raise ValueError(
f"delegation_depth must be >= 0; got {delegation_depth!r}"
)
return 1.0 / (1.0 + float(delegation_depth))


def derive_care_factors(
*,
animacy_class: AnimacyClass,
trajectory_class: TrajectoryClass,
delegation_depth: int,
alignment_protection: float,
vulnerability: float = 0.0,
) -> CareFactors:
"""Compose the four care factors from an entity's classifications.

Animacy from the class gradient, potential-trajectory from class +
vulnerability, bonding-proximity from delegation depth, and the supplied
alignment-protection (its own upstream signal). The result feeds
:func:`~substrate.care.care_weight.compute_care_weight`
unchanged — this only *derives* the factors, it does not alter the M1
self-weight bound or the categorical floor.
"""
if not 0.0 <= alignment_protection <= 1.0:
raise ValueError(
f"alignment_protection must be in [0, 1]; got {alignment_protection!r}"
)
return CareFactors(
animacy=score_for_class(animacy_class),
potential_trajectory=trajectory_gradient(
trajectory_class, vulnerability=vulnerability
),
bonding_proximity=bonding_gradient(delegation_depth),
alignment_protection=alignment_protection,
)


__all__ = [
"bonding_gradient",
"derive_care_factors",
"trajectory_gradient",
]
Loading
Loading