From 5427d4334c4e5c6d7ecbaffa79c041e97d450373 Mon Sep 17 00:00:00 2001 From: ExoHayvan <18trevor3695@gmail.com> Date: Sun, 11 Oct 2026 23:30:26 -0400 Subject: [PATCH 1/3] Prototype bounded evidence-based naming and simulated contributor contracts --- docs/capability-fixture.md | 107 +++++ docs/capability-naming-plan.md | 5 +- examples/capability_fixture_demo.py | 34 ++ scripts/peer_install_smoke.py | 25 ++ src/aethermesh_core/capability_fixture.py | 430 +++++++++++++++++++++ src/aethermesh_core/contributor_fixture.py | 114 ++++++ tests/fixtures/capability_naming.json | 144 +++++++ tests/fixtures/capability_naming_pins.json | 7 + tests/fixtures/contributor_activity.json | 86 +++++ tests/test_capability_fixture.py | 293 ++++++++++++++ tests/test_contributor_fixture.py | 139 +++++++ 11 files changed, 1382 insertions(+), 2 deletions(-) create mode 100644 docs/capability-fixture.md create mode 100644 examples/capability_fixture_demo.py create mode 100644 src/aethermesh_core/capability_fixture.py create mode 100644 src/aethermesh_core/contributor_fixture.py create mode 100644 tests/fixtures/capability_naming.json create mode 100644 tests/fixtures/capability_naming_pins.json create mode 100644 tests/fixtures/contributor_activity.json create mode 100644 tests/test_capability_fixture.py create mode 100644 tests/test_contributor_fixture.py diff --git a/docs/capability-fixture.md b/docs/capability-fixture.md new file mode 100644 index 00000000..8c324894 --- /dev/null +++ b/docs/capability-fixture.md @@ -0,0 +1,107 @@ +# Bounded naming and promotion fixture + +This is a pure SDK contract prototype for the [accepted roster and plan](capability-naming-plan.md). +It neither trains a model nor certifies live capabilities. Run it from a checkout +with the SDK installed (or `PYTHONPATH=src`): + +```sh +python examples/capability_fixture_demo.py +``` + +The example reads checked-in synthetic data from `tests/fixtures/` and prints an +eligible **simulated** proposal for Ash: internal capability version 3 → 4. Public +names contain no version suffix. The separate contributor fixture projects Ash +and Ember from synthetic execution events and labels the output simulated. These +are not eight trained available experts, actual routed participation or a model picker. +The example has no network, trainer, router, credential or deployment action. + +## Evidence contract + +`aethermesh_core.capability_fixture.evaluate_naming_fixture` takes bounded JSON +bytes, an external tuple of trusted **fixture** report digests and an external +expected policy digest. The sample stores pins separately in +`capability_naming_pins.json`. Selecting pins is a local test assumption, not +cryptographic evaluator provenance or publisher authorization. Do not derive trust +from a report's self-declared evaluator or silently accept its supplied digest. +`fixture_digest` is a helper for already bounded structured fixture data, not a +general untrusted-object serializer. + +The exact version-1 input contains: + +- Baseline and candidate profiles: immutable artifact ID, parent ID, stable lineage, + input/output schema commitment, trainer label and intended capability. +- Baseline and candidate reports: exact artifact/baseline/lineage/schema, evaluation, + dataset and taxonomy commitments, evaluator label, metric values and uncertainty. +- Externally pinned policy: exact baseline, current internal capability version, + evaluation/dataset/taxonomy commitments, allowed fixture evaluator labels and + complete metric rules (direction, required gain, tolerated regression, uncertainty + and candidate value bounds). +- Versioned taxonomy: capability labels, classification intervals and ordered name + candidates. Namespace: stable lineage-to-name assignments. Hints: run count and + self-description, intentionally ignored for decisions. + +A candidate must descend from the pinned baseline, differ in artifact identity and +preserve lineage, schema and intended role. Both reports must match all commitments; +changing evaluation or taxonomy requires a fresh matching pair. An allowed evaluator +label cannot equal either trainer label. A real identity/provenance system remains +necessary to establish that two differently named actors are independent. + +Metrics must match policy exactly. Conservative uncertainty intervals must satisfy +resource/value bounds, every regression budget and each required positive gain. +Decimal comparison avoids binary floating-point subtraction at policy boundaries. +Missing/conflicting evidence or failed gates returns experimental with no proposed +name or version increment. Passing evidence that does not classify the intended +capability returns unclassified. Internal versions stop at 4096. No training-run +count, flattering description or name change earns a version. + +Names remain stable for an existing lineage. New lineages use the first available +word in the classified capability's ordered pool. Names collide case-insensitively; +accepted roster words are reserved for their corresponding roles. Empty pools, +invalid words and namespace conflicts fail validation; exhausted usable words block +a proposal. This prototype chooses 1–32 ASCII letters as a bounded implementation +constraint, not a product language requirement. A proposal does not reserve a name +or allocate a version; durable concurrent allocation is future work. + +Results include exact input/policy/report commitments, baseline/candidate IDs, +measured labels, reasons and proposed internal version. `simulated=True` and +`promotion_committed`, `publication_authorized`, `live_capability_verified`, +`namespace_reserved` all false are explicit trust boundaries. No file or state is +modified. Eligible means only that this fixture satisfies its pinned fixture policy. + +## Contributor fixture contract + +`aethermesh_core.contributor_fixture.project_contributor_fixture` accepts version-1 +JSON containing a request ID, fixture clock and freshness bound, namespace, explicit +routes and events. A route binds request, attempt, artifact and lineage. Events must +match that exact route, increase sequence contiguously, preserve timestamp order and +obey selected → queued/executing → terminal transitions (failure/cancellation may +occur earlier). Executing heartbeats are allowed. Terminal attempts cannot resume. + +Only a fresh executing event contributes a deduplicated family name. Selected, +queued, missing, stale, completed, failed and cancelled attempts contribute none. +Unknown attempts, mismatched artifacts/requests, future events and duplicate or +out-of-order events are rejected. Event age applies only to simulated display +activity; it grants no ownership, reclamation or retention authority. Caller-created +events are not live evidence: `simulated=True`, `live_activity_verified=False`. +A future real adapter must consume authenticated actual routed participation and +cannot relabel these fixture outputs as live activity. + +## Limits and unfinished integration + +Both entrypoints accept at most 64 KiB of strict JSON (naming includes external +pin bytes in that bound), 16 nesting levels and reject duplicate keys/nonfinite +numbers/unknown fields. Naming allows 2 reports, 64 metrics, 32 taxonomy rules, +16 words per rule, 32 namespace entries, 16 evaluator labels and 64 external report +pins. Numeric metric magnitudes are at most 10^12; versions/revisions are bounded. +Contributor projection allows 32 routes and 64 events with a 1–3600 second fixture +freshness window. Cooperative cancellation is checked before and between bounded +work and before returning; there are no background workers or I/O. + +Tests cover golden data, changed commitments, insufficient/regressed/uncertain or +resource-exceeding evidence, name stability/collisions, decimal boundaries, malformed +and oversized input, cancellation and contributor event bindings/transitions. The +installed-package acceptance also exercises both APIs with these same static files. +This is not measured model quality, training, small-device performance, distributed +promotion, independent identity, real contributor activity or autonomous operation. +Trainers/app orchestration remain external; publication and persistent trust still +require their own authority. Generic peer/storage use remains independent of AI. diff --git a/docs/capability-naming-plan.md b/docs/capability-naming-plan.md index 8a9a0995..df120a5e 100644 --- a/docs/capability-naming-plan.md +++ b/docs/capability-naming-plan.md @@ -1,7 +1,8 @@ # Capability naming and evidence-based promotion plan -**Status: initial names and intended roles accepted; implementations and learned -capabilities remain planned.** This adds a staged AI-participation task alongside +**Status: initial names and intended roles accepted; a bounded deterministic +[naming/promotion fixture](capability-fixture.md) is implemented. Learned +capabilities, real evaluation provenance and committed promotion remain planned.** This adds a staged AI-participation task alongside current peer/storage work. The roster is not eight trained models already available. It does not change repository identity, transport trust, project publisher rules, generic SDK onboarding or resource ceilings. Trainers and application orchestration diff --git a/examples/capability_fixture_demo.py b/examples/capability_fixture_demo.py new file mode 100644 index 00000000..5df281e2 --- /dev/null +++ b/examples/capability_fixture_demo.py @@ -0,0 +1,34 @@ +"""Print simulated proposals from checked-in fixtures; never report live activity.""" + +import json +from dataclasses import asdict +from pathlib import Path + +from aethermesh_core.capability_fixture import evaluate_naming_fixture +from aethermesh_core.contributor_fixture import project_contributor_fixture + + +def main() -> None: + fixtures = Path(__file__).resolve().parents[1] / "tests" / "fixtures" + pins = json.loads((fixtures / "capability_naming_pins.json").read_bytes()) + decision = evaluate_naming_fixture( + (fixtures / "capability_naming.json").read_bytes(), + trusted_report_digests=tuple(pins["reports"]), + expected_policy_digest=pins["policy"], + ) + activity = project_contributor_fixture( + (fixtures / "contributor_activity.json").read_bytes() + ) + print( + json.dumps( + { + "simulated_proposal": asdict(decision), + "simulated_contributor_names": asdict(activity), + }, + indent=2, + ) + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/peer_install_smoke.py b/scripts/peer_install_smoke.py index eb0fb759..c29a2a6b 100644 --- a/scripts/peer_install_smoke.py +++ b/scripts/peer_install_smoke.py @@ -23,6 +23,30 @@ ROOT = Path(__file__).resolve().parents[1] +def capability_fixture_smoke() -> None: + """Check installed pure fixture APIs; these are not trained or live experts.""" + from aethermesh_core.capability_fixture import evaluate_naming_fixture + from aethermesh_core.contributor_fixture import project_contributor_fixture + + fixtures = ROOT / "tests" / "fixtures" + pins = json.loads((fixtures / "capability_naming_pins.json").read_bytes()) + result = evaluate_naming_fixture( + (fixtures / "capability_naming.json").read_bytes(), + trusted_report_digests=tuple(pins["reports"]), + expected_policy_digest=pins["policy"], + ) + assert result.status == "eligible" and result.proposed_name == "Ash" + assert (result.current_internal_version, result.proposed_internal_version) == (3, 4) + assert result.simulated and not result.promotion_committed + assert not result.publication_authorized and not result.live_capability_verified + assert not result.namespace_reserved + activity = project_contributor_fixture( + (fixtures / "contributor_activity.json").read_bytes() + ) + assert activity.names == ("Ash", "Ember") + assert activity.simulated and not activity.live_activity_verified + + def learning_state_smoke(cwd: Path) -> None: """Exercise installed local recovery independently of all peer transports.""" from aethermesh_core.learning_state import ( @@ -1113,6 +1137,7 @@ async def rejected( directory_smoke(cwd) ingestion_smoke(cwd) repair_plan_smoke(cwd) + capability_fixture_smoke() learning_state_smoke(cwd) await numeric_expert_smoke(cwd) await content_smoke(peer, pki, pins, cwd) diff --git a/src/aethermesh_core/capability_fixture.py b/src/aethermesh_core/capability_fixture.py new file mode 100644 index 00000000..4cf6aa56 --- /dev/null +++ b/src/aethermesh_core/capability_fixture.py @@ -0,0 +1,430 @@ +"""Bounded, pure naming/promotion fixtures; no training or publication authority.""" + +from __future__ import annotations + +import hashlib +import json +import re +from dataclasses import dataclass +from decimal import Decimal +from typing import Any, Literal + +from .artifact_codec import CheckCancel, _cancel, _integer +from .network.config import validate_fingerprint, validate_project_id +from .network.errors import ProtocolError +from .network.protocol import _constant, _object, _validate_message + +MAX_FIXTURE_BYTES = 65536 +ACCEPTED_FAMILIES = ( + ("routing", "Compass"), + ("planning", "Loom"), + ("resources", "Tempo"), + ("python", "Ash"), + ("typescript", "Ember"), + ("evaluation", "Lens"), + ("data", "Grove"), + ("monitoring", "Vigil"), +) + + +def fixture_digest(value: Any) -> str: + """Commit already bounded fixture data; not an untrusted object serializer. + + A digest identifies fixture bytes, never evaluator or publisher authority. + """ + raw = json.dumps( + value, sort_keys=True, separators=(",", ":"), ensure_ascii=True, allow_nan=False + ).encode("ascii") + if len(raw) > MAX_FIXTURE_BYTES: + raise ValueError("fixture commitment exceeds byte bound") + return hashlib.sha256(raw).hexdigest() + + +def _decode(raw: bytes) -> dict[str, Any]: + if type(raw) is not bytes or not 0 < len(raw) <= MAX_FIXTURE_BYTES: + raise ValueError("fixture input exceeds byte bound") + try: + value = json.loads( + raw.decode("utf-8"), object_pairs_hook=_object, parse_constant=_constant + ) + _validate_message(value) + except (ValueError, RecursionError, ProtocolError) as error: + raise ValueError("invalid bounded fixture JSON") from error + return _fields(value, set(value)) + + +def _fields(value: Any, keys: set[str]) -> dict[str, Any]: + if not isinstance(value, dict) or set(value) != keys: + raise ValueError("unexpected fixture fields") + return value + + +def _items(value: Any, maximum: int, minimum: int = 0) -> list[Any]: + if not isinstance(value, list) or not minimum <= len(value) <= maximum: + raise ValueError("fixture item bound") + return value + + +def _identifier(value: Any) -> str: + validate_project_id(value) + return str(value) + + +def _digest(value: Any) -> str: + validate_fingerprint(value) + return str(value) + + +def _number(value: Any) -> Decimal: + if type(value) not in (int, float): + raise ValueError("fixture metric must be a finite number") + result = Decimal(str(value)) + if not result.is_finite() or abs(result) > 10**12: + raise ValueError("fixture metric magnitude bound") + return result + + +def _word(value: Any) -> str: + if not isinstance(value, str) or re.fullmatch(r"[A-Za-z]{1,32}", value) is None: + raise ValueError("fixture name must be one word of 1–32 ASCII letters") + return value + + +def _namespace(value: Any) -> dict[str, str]: + result: dict[str, str] = {} + words: set[str] = set() + for item in _items(value, 32): + item = _fields(item, {"lineage", "name"}) + lineage, word = _identifier(item["lineage"]), _word(item["name"]) + if lineage in result or word.casefold() in words: + raise ValueError("conflicting fixture namespace") + result[lineage] = word + words.add(word.casefold()) + return result + + +def _profile(value: Any) -> dict[str, Any]: + row = _fields( + value, {"artifact", "parent", "lineage", "schema", "trainer", "intended"} + ) + for key in ("artifact", "schema"): + _digest(row[key]) + if row["parent"] is not None: + _digest(row["parent"]) + for key in ("lineage", "trainer", "intended"): + _identifier(row[key]) + return row + + +def _report(value: Any) -> dict[str, Any]: + row = _fields( + value, + { + "artifact", + "baseline", + "lineage", + "schema", + "evaluation", + "dataset", + "taxonomy", + "evaluator", + "metrics", + }, + ) + for key in ("artifact", "baseline", "schema", "evaluation", "dataset", "taxonomy"): + _digest(row[key]) + for key in ("lineage", "evaluator"): + _identifier(row[key]) + metrics = row["metrics"] + if not isinstance(metrics, dict) or not 1 <= len(metrics) <= 64: + raise ValueError("fixture metric count bound") + for name, reading in metrics.items(): + _identifier(name) + _fields(reading, {"value", "uncertainty"}) + _number(reading["value"]) + if _number(reading["uncertainty"]) < 0: + raise ValueError("negative fixture uncertainty") + return row + + +def _taxonomy(value: Any) -> dict[str, dict[str, Any]]: + taxonomy = _fields(value, {"revision", "rules"}) + _integer(taxonomy["revision"], 1, 4096) + result = {} + for rule in _items(taxonomy["rules"], 32, 1): + _fields(rule, {"label", "metric", "minimum", "maximum", "words"}) + label = _identifier(rule["label"]) + _identifier(rule["metric"]) + if label in result or _number(rule["minimum"]) > _number(rule["maximum"]): + raise ValueError("conflicting fixture taxonomy") + words = [_word(word) for word in _items(rule["words"], 16, 1)] + if len({word.casefold() for word in words}) != len(words): + raise ValueError("duplicate fixture words") + result[label] = rule + return result + + +def _policy(value: Any) -> dict[str, dict[str, Any]]: + policy = _fields( + value, + { + "baseline", + "baseline_version", + "evaluation", + "dataset", + "taxonomy", + "evaluators", + "metrics", + }, + ) + for key in ("baseline", "evaluation", "dataset", "taxonomy"): + _digest(policy[key]) + _integer(policy["baseline_version"], 0, 4096) + evaluators = [_identifier(item) for item in _items(policy["evaluators"], 16, 1)] + if len(set(evaluators)) != len(evaluators): + raise ValueError("duplicate fixture evaluators") + result = {} + for rule in _items(policy["metrics"], 64, 1): + _fields( + rule, + { + "name", + "direction", + "gain", + "regression", + "uncertainty", + "minimum", + "maximum", + }, + ) + name = _identifier(rule["name"]) + if name in result or rule["direction"] not in ("higher", "lower"): + raise ValueError("conflicting fixture metric policy") + if any( + _number(rule[key]) < 0 for key in ("gain", "regression", "uncertainty") + ) or _number(rule["minimum"]) > _number(rule["maximum"]): + raise ValueError("invalid fixture metric limits") + result[name] = rule + if not any(_number(rule["gain"]) > 0 for rule in result.values()): + raise ValueError("fixture promotion must require a measurable gain") + return result + + +@dataclass(frozen=True) +class FixtureDecision: + status: Literal["eligible", "experimental", "unclassified"] + reasons: tuple[str, ...] + proposed_name: str | None + measured_capabilities: tuple[str, ...] + current_internal_version: int + proposed_internal_version: int + baseline_artifact: str + candidate_artifact: str + input_digest: str + policy_digest: str + report_digests: tuple[str, str] + simulated: Literal[True] = True + promotion_committed: Literal[False] = False + publication_authorized: Literal[False] = False + live_capability_verified: Literal[False] = False + namespace_reserved: Literal[False] = False + + +def _bindings( + data: dict[str, Any], + baseline: dict[str, Any], + candidate: dict[str, Any], + reports: tuple[dict[str, Any], dict[str, Any]], + report_ids: tuple[str, str], + trusted: tuple[str, ...], +) -> list[str]: + policy = data["policy"] + reasons = [] + if ( + baseline["artifact"] != policy["baseline"] + or candidate["parent"] != baseline["artifact"] + or candidate["artifact"] == baseline["artifact"] + or any( + candidate[key] != baseline[key] for key in ("lineage", "schema", "intended") + ) + ): + reasons.append("profile_binding") + if fixture_digest(data["taxonomy"]) != policy["taxonomy"]: + reasons.append("taxonomy_revision") + for profile, report, identity in zip( + (baseline, candidate), reports, report_ids, strict=True + ): + if ( + identity not in trusted + or report["evaluator"] not in policy["evaluators"] + or report["evaluator"] in (baseline["trainer"], candidate["trainer"]) + ): + reasons.append("unverified_fixture_report") + expected = { + "artifact": profile["artifact"], + "baseline": baseline["artifact"], + "lineage": profile["lineage"], + "schema": profile["schema"], + "evaluation": policy["evaluation"], + "dataset": policy["dataset"], + "taxonomy": policy["taxonomy"], + } + if any(report[key] != value for key, value in expected.items()): + reasons.append("report_binding") + return reasons + + +def _metric_reasons( + rules: dict[str, dict[str, Any]], + reports: tuple[dict[str, Any], dict[str, Any]], + check_cancel: CheckCancel | None, +) -> list[str]: + baseline, candidate = (report["metrics"] for report in reports) + if set(baseline) != set(rules) or set(candidate) != set(rules): + return ["metric_set"] + reasons = [] + for name, rule in rules.items(): + _cancel(check_cancel) + b, c = baseline[name], candidate[name] + bv, cv = _number(b["value"]), _number(c["value"]) + bu, cu = _number(b["uncertainty"]), _number(c["uncertainty"]) + if ( + max(bu, cu) > _number(rule["uncertainty"]) + or cv - cu < _number(rule["minimum"]) + or cv + cu > _number(rule["maximum"]) + ): + reasons.append("resource_or_uncertainty") + gain = cv - cu - bv - bu if rule["direction"] == "higher" else bv - bu - cv - cu + if gain < -_number(rule["regression"]): + reasons.append("regression") + if _number(rule["gain"]) > 0 and gain < _number(rule["gain"]): + reasons.append("insufficient_gain") + return reasons + + +def _name( + lineage: str, label: str, words: list[Any], namespace: dict[str, str] +) -> str | None: + reserved = {word.casefold(): role for role, word in ACCEPTED_FAMILIES} + candidates = [namespace[lineage]] if lineage in namespace else words + for candidate in candidates: + word = _word(candidate) + if word.casefold() in reserved and reserved[word.casefold()] != label: + continue + if lineage in namespace or word.casefold() not in { + name.casefold() for name in namespace.values() + }: + return word + return None + + +def evaluate_naming_fixture( + raw: bytes, + *, + trusted_report_digests: tuple[str, ...], + expected_policy_digest: str, + check_cancel: CheckCancel | None = None, +) -> FixtureDecision: + """Pure local proposal from explicitly pinned fixture reports and policy. + + Pin selection is simulated provenance, not independent real-world evaluation. + No version/name is committed. Callers must not treat this result as a release, + trained capability, live activity or authorization to publish. + """ + _cancel(check_cancel) + if ( + not isinstance(trusted_report_digests, tuple) + or not 1 <= len(trusted_report_digests) <= 64 + or len(set(trusted_report_digests)) != len(trusted_report_digests) + ): + raise ValueError("fixture report pin bound") + for identity in (*trusted_report_digests, expected_policy_digest): + _digest(identity) + decoded = _decode(raw) + if len(raw) + 64 * (len(trusted_report_digests) + 1) > MAX_FIXTURE_BYTES: + raise ValueError("aggregate fixture byte bound") + data = _fields( + decoded, + { + "version", + "baseline", + "candidate", + "reports", + "taxonomy", + "policy", + "namespace", + "hints", + }, + ) + _integer(data["version"], 1, 1) + baseline, candidate = _profile(data["baseline"]), _profile(data["candidate"]) + report_data = _fields(data["reports"], {"baseline", "candidate"}) + reports = (_report(report_data["baseline"]), _report(report_data["candidate"])) + report_ids = (fixture_digest(reports[0]), fixture_digest(reports[1])) + taxonomy, rules, namespace = ( + _taxonomy(data["taxonomy"]), + _policy(data["policy"]), + _namespace(data["namespace"]), + ) + hints = _fields(data["hints"], {"training_run_count", "self_description"}) + _integer(hints["training_run_count"], 0, 10**9) + if ( + not isinstance(hints["self_description"], str) + or len(hints["self_description"]) > 1024 + ): + raise ValueError("fixture description bound") + policy_id = fixture_digest(data["policy"]) + if policy_id != expected_policy_digest: + raise ValueError("fixture policy pin mismatch") + reasons = _bindings( + data, baseline, candidate, reports, report_ids, trusted_report_digests + ) + labels: list[str] = [] + name = None + if not reasons: + reasons.extend(_metric_reasons(rules, reports, check_cancel)) + if not reasons: + for label, rule in taxonomy.items(): + _cancel(check_cancel) + reading = reports[1]["metrics"].get(rule["metric"]) + if ( + reading is not None + and _number(reading["value"]) - _number(reading["uncertainty"]) + >= _number(rule["minimum"]) + and _number(reading["value"]) + _number(reading["uncertainty"]) + <= _number(rule["maximum"]) + ): + labels.append(label) + primary = candidate["intended"] + if primary not in labels: + reasons.append("unclassified") + else: + name = _name( + candidate["lineage"], primary, taxonomy[primary]["words"], namespace + ) + if name is None: + reasons.append("name_unavailable") + version = data["policy"]["baseline_version"] + if version == 4096: + reasons.append("version_exhausted") + _cancel(check_cancel) + status: Literal["eligible", "experimental", "unclassified"] = ( + "unclassified" + if "unclassified" in reasons + else "experimental" + if reasons + else "eligible" + ) + return FixtureDecision( + status, + tuple(sorted(set(reasons))), + None if reasons else name, + tuple(sorted(labels)), + version, + version if reasons else version + 1, + baseline["artifact"], + candidate["artifact"], + fixture_digest(data), + policy_id, + report_ids, + ) diff --git a/src/aethermesh_core/contributor_fixture.py b/src/aethermesh_core/contributor_fixture.py new file mode 100644 index 00000000..261c315d --- /dev/null +++ b/src/aethermesh_core/contributor_fixture.py @@ -0,0 +1,114 @@ +"""Pure simulated contributor projection; caller events are not live activity proof.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Literal + +from .artifact_codec import CheckCancel, _cancel, _integer +from .capability_fixture import ( + _decode, + _digest, + _fields, + _identifier, + _items, + _namespace, +) + +_TRANSITIONS = { + "selected": {"queued", "executing", "failed", "cancelled"}, + "queued": {"executing", "failed", "cancelled"}, + "executing": {"executing", "completed", "failed", "cancelled"}, + "completed": set(), + "failed": set(), + "cancelled": set(), +} + + +@dataclass(frozen=True) +class FixtureContributors: + request_id: str + names: tuple[str, ...] + simulated: Literal[True] = True + live_activity_verified: Literal[False] = False + + +def _routes( + data: dict[str, Any], namespace: dict[str, str] +) -> dict[str, dict[str, Any]]: + result = {} + for row in _items(data["routes"], 32): + _fields(row, {"request_id", "attempt_id", "artifact", "lineage"}) + attempt = _identifier(row["attempt_id"]) + _digest(row["artifact"]) + lineage = _identifier(row["lineage"]) + if ( + row["request_id"] != data["request_id"] + or attempt in result + or lineage not in namespace + ): + raise ValueError("conflicting fixture route") + result[attempt] = row + return result + + +def project_contributor_fixture( + raw: bytes, *, check_cancel: CheckCancel | None = None +) -> FixtureContributors: + """Project bounded simulated routed events, never advertise live contributors. + + Selected/queued routes alone produce no active name. Only a fresh executing + event for the exact request, attempt and artifact contributes a family name. + Inputs are synthetic and confer no execution or routing authority. + """ + _cancel(check_cancel) + data = _fields( + _decode(raw), + { + "version", + "request_id", + "now", + "max_event_age", + "namespace", + "routes", + "events", + }, + ) + _integer(data["version"], 1, 1) + request = _digest(data["request_id"]) + _integer(data["now"], 1, 4102444800) + _integer(data["max_event_age"], 1, 3600) + namespace = _namespace(data["namespace"]) + routes = _routes(data, namespace) + states = dict.fromkeys(routes, (0, "selected", 0)) + for event in _items(data["events"], 64): + _cancel(check_cancel) + _fields( + event, {"request_id", "attempt_id", "artifact", "sequence", "state", "at"} + ) + attempt = _identifier(event["attempt_id"]) + _digest(event["artifact"]) + state = _identifier(event["state"]) + _integer(event["sequence"], 1, 64) + _integer(event["at"], 1, data["now"]) + if ( + attempt not in routes + or event["request_id"] != request + or event["artifact"] != routes[attempt]["artifact"] + ): + raise ValueError("unbound fixture event") + sequence, previous, at = states[attempt] + if ( + event["sequence"] != sequence + 1 + or event["at"] < at + or state not in _TRANSITIONS[previous] + ): + raise ValueError("invalid fixture event order or transition") + states[attempt] = (event["sequence"], state, event["at"]) + names = { + namespace[routes[attempt]["lineage"]] + for attempt, (_, state, at) in states.items() + if state == "executing" and data["now"] - at <= data["max_event_age"] + } + _cancel(check_cancel) + return FixtureContributors(request, tuple(sorted(names))) diff --git a/tests/fixtures/capability_naming.json b/tests/fixtures/capability_naming.json new file mode 100644 index 00000000..a9c757cb --- /dev/null +++ b/tests/fixtures/capability_naming.json @@ -0,0 +1,144 @@ +{ + "version": 1, + "baseline": { + "artifact": "1111111111111111111111111111111111111111111111111111111111111111", + "parent": null, + "lineage": "python-family", + "schema": "3333333333333333333333333333333333333333333333333333333333333333", + "trainer": "fixture-trainer", + "intended": "python" + }, + "candidate": { + "artifact": "2222222222222222222222222222222222222222222222222222222222222222", + "parent": "1111111111111111111111111111111111111111111111111111111111111111", + "lineage": "python-family", + "schema": "3333333333333333333333333333333333333333333333333333333333333333", + "trainer": "fixture-trainer", + "intended": "python" + }, + "reports": { + "baseline": { + "artifact": "1111111111111111111111111111111111111111111111111111111111111111", + "baseline": "1111111111111111111111111111111111111111111111111111111111111111", + "lineage": "python-family", + "schema": "3333333333333333333333333333333333333333333333333333333333333333", + "evaluation": "4444444444444444444444444444444444444444444444444444444444444444", + "dataset": "5555555555555555555555555555555555555555555555555555555555555555", + "taxonomy": "310fdfdba5c487fc4c1e26621c9fd28cc676a62637a065139df594d07c4b30c4", + "evaluator": "fixture-evaluator", + "metrics": { + "accuracy": { + "value": 0.8, + "uncertainty": 0.005 + }, + "memory": { + "value": 1024, + "uncertainty": 0 + } + } + }, + "candidate": { + "artifact": "2222222222222222222222222222222222222222222222222222222222222222", + "baseline": "1111111111111111111111111111111111111111111111111111111111111111", + "lineage": "python-family", + "schema": "3333333333333333333333333333333333333333333333333333333333333333", + "evaluation": "4444444444444444444444444444444444444444444444444444444444444444", + "dataset": "5555555555555555555555555555555555555555555555555555555555555555", + "taxonomy": "310fdfdba5c487fc4c1e26621c9fd28cc676a62637a065139df594d07c4b30c4", + "evaluator": "fixture-evaluator", + "metrics": { + "accuracy": { + "value": 0.9, + "uncertainty": 0.005 + }, + "memory": { + "value": 900, + "uncertainty": 0 + } + } + } + }, + "taxonomy": { + "revision": 1, + "rules": [ + { + "label": "python", + "metric": "accuracy", + "minimum": 0.7, + "maximum": 1, + "words": [ + "Ash", + "Cedar" + ] + } + ] + }, + "policy": { + "baseline": "1111111111111111111111111111111111111111111111111111111111111111", + "baseline_version": 3, + "evaluation": "4444444444444444444444444444444444444444444444444444444444444444", + "dataset": "5555555555555555555555555555555555555555555555555555555555555555", + "taxonomy": "310fdfdba5c487fc4c1e26621c9fd28cc676a62637a065139df594d07c4b30c4", + "evaluators": [ + "fixture-evaluator" + ], + "metrics": [ + { + "name": "accuracy", + "direction": "higher", + "gain": 0.05, + "regression": 0, + "uncertainty": 0.01, + "minimum": 0, + "maximum": 1 + }, + { + "name": "memory", + "direction": "lower", + "gain": 0, + "regression": 0, + "uncertainty": 0, + "minimum": 0, + "maximum": 2048 + } + ] + }, + "namespace": [ + { + "lineage": "routing-family", + "name": "Compass" + }, + { + "lineage": "planning-family", + "name": "Loom" + }, + { + "lineage": "resources-family", + "name": "Tempo" + }, + { + "lineage": "python-family", + "name": "Ash" + }, + { + "lineage": "typescript-family", + "name": "Ember" + }, + { + "lineage": "evaluation-family", + "name": "Lens" + }, + { + "lineage": "data-family", + "name": "Grove" + }, + { + "lineage": "monitoring-family", + "name": "Vigil" + } + ], + "hints": { + "training_run_count": 1, + "self_description": "Synthetic fixture only." + } +} diff --git a/tests/fixtures/capability_naming_pins.json b/tests/fixtures/capability_naming_pins.json new file mode 100644 index 00000000..5e150934 --- /dev/null +++ b/tests/fixtures/capability_naming_pins.json @@ -0,0 +1,7 @@ +{ + "policy": "7bf9482448d1f9da39af1063f2457d51dcfb0e8063dae413d06e5c78def71f29", + "reports": [ + "1609109e68ee643e90235227ca46897f5278cf74651a970115e791ed6e86a875", + "ea53ce906eff1e18ec06ee2aa1c61c3dc3665c5cf51a803871632a2f5556e3d0" + ] +} diff --git a/tests/fixtures/contributor_activity.json b/tests/fixtures/contributor_activity.json new file mode 100644 index 00000000..86e9fb86 --- /dev/null +++ b/tests/fixtures/contributor_activity.json @@ -0,0 +1,86 @@ +{ + "version": 1, + "request_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "now": 1000, + "max_event_age": 30, + "namespace": [ + { + "lineage": "routing-family", + "name": "Compass" + }, + { + "lineage": "planning-family", + "name": "Loom" + }, + { + "lineage": "resources-family", + "name": "Tempo" + }, + { + "lineage": "python-family", + "name": "Ash" + }, + { + "lineage": "typescript-family", + "name": "Ember" + }, + { + "lineage": "evaluation-family", + "name": "Lens" + }, + { + "lineage": "data-family", + "name": "Grove" + }, + { + "lineage": "monitoring-family", + "name": "Vigil" + } + ], + "routes": [ + { + "request_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "attempt_id": "python", + "artifact": "1111111111111111111111111111111111111111111111111111111111111111", + "lineage": "python-family" + }, + { + "request_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "attempt_id": "typescript", + "artifact": "2222222222222222222222222222222222222222222222222222222222222222", + "lineage": "typescript-family" + }, + { + "request_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "attempt_id": "planning", + "artifact": "3333333333333333333333333333333333333333333333333333333333333333", + "lineage": "planning-family" + } + ], + "events": [ + { + "request_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "attempt_id": "python", + "artifact": "1111111111111111111111111111111111111111111111111111111111111111", + "sequence": 1, + "state": "executing", + "at": 990 + }, + { + "request_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "attempt_id": "typescript", + "artifact": "2222222222222222222222222222222222222222222222222222222222222222", + "sequence": 1, + "state": "executing", + "at": 990 + }, + { + "request_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "attempt_id": "planning", + "artifact": "3333333333333333333333333333333333333333333333333333333333333333", + "sequence": 1, + "state": "queued", + "at": 990 + } + ] +} diff --git a/tests/test_capability_fixture.py b/tests/test_capability_fixture.py new file mode 100644 index 00000000..128abcee --- /dev/null +++ b/tests/test_capability_fixture.py @@ -0,0 +1,293 @@ +"""Synthetic evidence must never become publication or live capability authority.""" + +import copy +import json +import unittest +from pathlib import Path +from unittest.mock import patch + +from aethermesh_core import capability_fixture as naming + +FIXTURES = Path(__file__).parent / "fixtures" + + +def fixture(): + return json.loads((FIXTURES / "capability_naming.json").read_bytes()) + + +def pins(data): + return { + "trusted_report_digests": tuple( + naming.fixture_digest(data["reports"][key]) + for key in ("baseline", "candidate") + ), + "expected_policy_digest": naming.fixture_digest(data["policy"]), + } + + +def evaluate(data, **options): + return naming.evaluate_naming_fixture( + json.dumps(data).encode(), **(pins(data) | options) + ) + + +def retax(data): + identity = naming.fixture_digest(data["taxonomy"]) + data["policy"]["taxonomy"] = identity + for report in data["reports"].values(): + report["taxonomy"] = identity + + +class NamingFixtureTests(unittest.TestCase): + def test_golden_installed_contract_and_purity(self): + raw = (FIXTURES / "capability_naming.json").read_bytes() + stored = json.loads((FIXTURES / "capability_naming_pins.json").read_bytes()) + with ( + patch("builtins.open", side_effect=AssertionError("I/O")), + patch("socket.socket", side_effect=AssertionError("network")), + patch("subprocess.Popen", side_effect=AssertionError("execution")), + ): + result = naming.evaluate_naming_fixture( + raw, + trusted_report_digests=tuple(stored["reports"]), + expected_policy_digest=stored["policy"], + ) + self.assertEqual(result.status, "eligible") + self.assertEqual(result.proposed_name, "Ash") + self.assertEqual(result.measured_capabilities, ("python",)) + self.assertEqual( + (result.current_internal_version, result.proposed_internal_version), (3, 4) + ) + self.assertNotEqual(result.baseline_artifact, result.candidate_artifact) + self.assertTrue(result.simulated) + self.assertFalse(result.promotion_committed or result.publication_authorized) + self.assertFalse(result.live_capability_verified or result.namespace_reserved) + self.assertEqual(result, naming.evaluate_naming_fixture(raw, **pins(fixture()))) + + def test_run_count_and_self_description_never_award_gains(self): + data = fixture() + before = evaluate(data) + data["hints"] = {"training_run_count": 999, "self_description": "I am perfect"} + after = evaluate(data) + self.assertEqual(before.proposed_name, after.proposed_name) + self.assertEqual( + before.proposed_internal_version, after.proposed_internal_version + ) + self.assertNotEqual(before.input_digest, after.input_digest) + data["reports"]["candidate"]["metrics"]["accuracy"]["value"] = 0.8 + self.assertEqual(evaluate(data).proposed_internal_version, 3) + + def test_profile_and_report_binding(self): + cases = [ + ("baseline", "artifact", "6" * 64), + ("candidate", "parent", "6" * 64), + ("candidate", "artifact", "1" * 64), + ("candidate", "lineage", "other"), + ("candidate", "schema", "6" * 64), + ("candidate", "intended", "other"), + ] + for where, key, value in cases: + with self.subTest(where=where, key=key): + data = fixture() + data[where][key] = value + self.assertIn("profile_binding", evaluate(data).reasons) + for which in ("baseline", "candidate"): + for key in ( + "artifact", + "baseline", + "schema", + "evaluation", + "dataset", + "taxonomy", + "lineage", + ): + with self.subTest(which=which, key=key): + data = fixture() + data["reports"][which][key] = "6" * 64 + result = evaluate(data) + self.assertIn("report_binding", result.reasons) + self.assertIsNone(result.proposed_name) + self.assertEqual(result.proposed_internal_version, 3) + + def test_independent_fixture_pins_and_evaluator(self): + for evaluator in ("unapproved", "fixture-trainer"): + with self.subTest(evaluator=evaluator): + data = fixture() + data["policy"]["evaluators"].append("fixture-trainer") + data["reports"]["candidate"]["evaluator"] = evaluator + self.assertIn("unverified_fixture_report", evaluate(data).reasons) + data = fixture() + old = pins(data) + data["reports"]["candidate"]["metrics"]["accuracy"]["value"] = 0.95 + self.assertIn("unverified_fixture_report", evaluate(data, **old).reasons) + data["policy"]["baseline_version"] += 1 + with self.assertRaisesRegex(ValueError, "policy pin"): + evaluate(data, **old) + + def test_evaluation_and_taxonomy_revision_require_fresh_pair(self): + for key in ("evaluation", "dataset"): + data = fixture() + data["policy"][key] = "6" * 64 + data["reports"]["candidate"][key] = "6" * 64 + self.assertIn("report_binding", evaluate(data).reasons) + data["reports"]["baseline"][key] = "6" * 64 + self.assertEqual(evaluate(data).status, "eligible") + data = fixture() + data["taxonomy"]["revision"] += 1 + self.assertIn("taxonomy_revision", evaluate(data).reasons) + retax(data) + self.assertEqual(evaluate(data).status, "eligible") + + def test_metric_evidence_gates(self): + for metric, field, value, reason in ( + ("accuracy", "value", 0.82, "insufficient_gain"), + ("accuracy", "value", 0.7, "regression"), + ("accuracy", "uncertainty", 0.02, "resource_or_uncertainty"), + ("memory", "value", 3000, "resource_or_uncertainty"), + ("memory", "value", -1, "resource_or_uncertainty"), + ("memory", "value", 1100, "regression"), + ): + with self.subTest(metric=metric, field=field, value=value): + data = fixture() + data["reports"]["candidate"]["metrics"][metric][field] = value + result = evaluate(data) + self.assertIn(reason, result.reasons) + self.assertEqual(result.status, "experimental") + self.assertEqual(result.proposed_internal_version, 3) + self.assertIsNone(result.proposed_name) + self.assertEqual(result.measured_capabilities, ()) + for which in ("baseline", "candidate"): + data = fixture() + data["reports"][which]["metrics"].pop("memory") + self.assertIn("metric_set", evaluate(data).reasons) + + def test_decimal_boundary_lower_gain_and_regression_allowance(self): + data = fixture() + for report in data["reports"].values(): + report["metrics"]["accuracy"]["uncertainty"] = 0 + data["reports"]["candidate"]["metrics"]["accuracy"]["value"] = 0.85 + data["policy"]["metrics"][1]["gain"] = 124 + self.assertEqual(evaluate(data).status, "eligible") + data["policy"]["metrics"][1].update(gain=0, regression=76) + data["reports"]["candidate"]["metrics"]["memory"]["value"] = 1100 + self.assertEqual(evaluate(data).status, "eligible") + + def test_classification_and_version_ceiling(self): + for update in ({"minimum": 0.95}, {"maximum": 0.85}, {"metric": "unknown"}): + data = fixture() + data["taxonomy"]["rules"][0].update(update) + retax(data) + result = evaluate(data) + self.assertEqual(result.status, "unclassified") + self.assertIsNone(result.proposed_name) + data = fixture() + data["policy"]["baseline_version"] = 4096 + self.assertIn("version_exhausted", evaluate(data).reasons) + self.assertEqual(evaluate(data).proposed_internal_version, 4096) + + def test_stable_names_collisions_and_reserved_roster(self): + data = fixture() + data["taxonomy"]["rules"][0]["words"] = ["Cedar"] + retax(data) + self.assertEqual(evaluate(data).proposed_name, "Ash") + data["namespace"] = [{"lineage": "other", "name": "cEdAr"}] + self.assertIn("name_unavailable", evaluate(data).reasons) + data["taxonomy"]["rules"][0]["words"] = ["Compass", "Cedar", "Birch"] + retax(data) + self.assertEqual(evaluate(data).proposed_name, "Birch") + data["namespace"] = [{"lineage": "python-family", "name": "Compass"}] + self.assertIn("name_unavailable", evaluate(data).reasons) + self.assertEqual( + tuple(word for _, word in naming.ACCEPTED_FAMILIES), + ("Compass", "Loom", "Tempo", "Ash", "Ember", "Lens", "Grove", "Vigil"), + ) + + def test_malformed_bounded_inputs(self): + for raw in ( + b"", + b"[]", + b"{", + b'{"x":1,"x":2}', + b'{"x":NaN}', + b'{"x":1e999}', + b'{"x":"\\ud800"}', + b"\xff", + b"x" * 65537, + b'{"x":' + b"[" * 20 + b"0" + b"]" * 20 + b"}", + ): + with self.subTest(raw=raw[:30]), self.assertRaises(ValueError): + naming.evaluate_naming_fixture(raw, **pins(fixture())) + raw = json.dumps(fixture()).encode() + with self.assertRaisesRegex(ValueError, "aggregate"): + naming.evaluate_naming_fixture( + raw + b" " * (65500 - len(raw)), **pins(fixture()) + ) + for trusted in ((), [], ("1" * 64,) * 2, ("wrong",)): + with self.subTest(trusted=trusted), self.assertRaises(ValueError): + evaluate(fixture(), trusted_report_digests=trusted) + with self.assertRaises(ValueError): + naming.fixture_digest("x" * 65536) + + def test_schema_limits_and_conflicts(self): + changes = [ + (("version",), 2), + (("extra",), 1), + ( + ("namespace",), + [{"lineage": "a", "name": "Ash"}, {"lineage": "b", "name": "ash"}], + ), + ( + ("namespace",), + [{"lineage": "a", "name": "Ash"}, {"lineage": "a", "name": "Cedar"}], + ), + (("namespace",), [{"lineage": "a", "name": "Ash2"}]), + (("namespace",), [{}] * 33), + (("hints", "training_run_count"), True), + (("hints", "self_description"), "x" * 1025), + (("reports", "candidate", "metrics"), {}), + (("reports", "candidate", "metrics", "accuracy", "value"), True), + (("reports", "candidate", "metrics", "accuracy", "value"), 10**13), + (("reports", "candidate", "metrics", "accuracy", "uncertainty"), -1), + (("policy", "evaluators"), ["same", "same"]), + (("policy", "metrics", 0, "direction"), "unknown"), + (("policy", "metrics", 0, "gain"), -1), + (("policy", "metrics", 0, "minimum"), 2), + (("policy", "metrics", 0, "gain"), 0), + (("taxonomy", "rules", 0, "words"), ["Ash", "ash"]), + (("taxonomy", "rules", 0, "minimum"), 2), + ] + for path, value in changes: + with self.subTest(path=path, value=value): + data = fixture() + target = data + for key in path[:-1]: + target = target[key] + target[path[-1]] = value + with self.assertRaises(ValueError): + evaluate(data) + for section in ("policy", "taxonomy"): + data = fixture() + key = "metrics" if section == "policy" else "rules" + data[section][key].append(copy.deepcopy(data[section][key][0])) + with self.assertRaises(ValueError): + evaluate(data) + + def test_cancellation_at_every_checkpoint(self): + count = 0 + + def record(): + nonlocal count + count += 1 + + evaluate(fixture(), check_cancel=record) + for stop in range(1, count + 1): + current = 0 + + def cancel(stop=stop): + nonlocal current + current += 1 + if current == stop: + raise InterruptedError("cancelled") + + with self.subTest(stop=stop), self.assertRaises(InterruptedError): + evaluate(fixture(), check_cancel=cancel) diff --git a/tests/test_contributor_fixture.py b/tests/test_contributor_fixture.py new file mode 100644 index 00000000..8cb1994c --- /dev/null +++ b/tests/test_contributor_fixture.py @@ -0,0 +1,139 @@ +"""No contributor name appears without matching simulated execution evidence.""" + +import copy +import json +import unittest + +from aethermesh_core.contributor_fixture import project_contributor_fixture +from tests.test_capability_fixture import FIXTURES + + +def fixture(): + return json.loads((FIXTURES / "contributor_activity.json").read_bytes()) + + +def project(data, **options): + return project_contributor_fixture(json.dumps(data).encode(), **options) + + +class ContributorFixtureTests(unittest.TestCase): + def test_golden_simulated_only_and_no_selected_queued_activity(self): + data = fixture() + result = project(data) + self.assertEqual(result.names, ("Ash", "Ember")) + self.assertEqual(result.request_id, data["request_id"]) + self.assertTrue(result.simulated) + self.assertFalse(result.live_activity_verified) + data["events"] = [] + self.assertEqual(project(data).names, ()) + data["routes"] = [] + self.assertEqual(project(data).names, ()) + + def test_queued_heartbeat_terminal_and_stale(self): + for terminal in ("completed", "failed", "cancelled"): + with self.subTest(terminal=terminal): + data = fixture() + event = data["events"][0] + data["events"] = [ + {**event, "state": "queued", "at": 980}, + {**event, "sequence": 2, "at": 990}, + {**event, "sequence": 3, "at": 1000}, + ] + self.assertEqual(project(data).names, ("Ash",)) + data["events"].append( + {**event, "sequence": 4, "state": terminal, "at": 1000} + ) + self.assertEqual(project(data).names, ()) + data = fixture() + data["now"] = 1020 + self.assertEqual(project(data).names, ("Ash", "Ember")) + data["now"] = 1021 + self.assertEqual(project(data).names, ()) + for initial in ("queued", "failed", "cancelled"): + data = fixture() + for event in data["events"]: + event["state"] = initial + self.assertEqual(project(data).names, ()) + + def test_multiple_attempts_one_family_name(self): + data = fixture() + data["routes"].append({**data["routes"][0], "attempt_id": "hedge"}) + data["events"].append({**data["events"][0], "attempt_id": "hedge"}) + self.assertEqual(project(data).names, ("Ash", "Ember")) + + def test_route_bindings_and_limits(self): + for key, value in ( + ("request_id", "b" * 64), + ("lineage", "unknown"), + ("artifact", "bad"), + ): + data = fixture() + data["routes"][0][key] = value + with self.subTest(key=key), self.assertRaises(ValueError): + project(data) + data = fixture() + data["routes"].append(copy.deepcopy(data["routes"][0])) + with self.assertRaises(ValueError): + project(data) + for key, value in ( + ("routes", [{}] * 33), + ("events", [{}] * 65), + ("now", 0), + ("max_event_age", 3601), + ("version", 2), + ): + data = fixture() + data[key] = value + with self.subTest(key=key), self.assertRaises(ValueError): + project(data) + + def test_event_binding_order_and_state(self): + for key, value in ( + ("attempt_id", "unknown"), + ("request_id", "b" * 64), + ("artifact", "b" * 64), + ("sequence", 2), + ("state", "completed"), + ("state", "unknown"), + ("at", 1001), + ): + data = fixture() + data["events"][0][key] = value + with self.subTest(key=key, value=value), self.assertRaises(ValueError): + project(data) + for appended in ( + {"sequence": 1}, + {"sequence": 2, "at": 989}, + {"sequence": 2, "state": "queued"}, + ): + data = fixture() + data["events"].append(data["events"][0] | appended) + with self.subTest(appended=appended), self.assertRaises(ValueError): + project(data) + data = fixture() + event = data["events"][0] + data["events"].extend( + [{**event, "sequence": 2, "state": "completed"}, {**event, "sequence": 3}] + ) + with self.assertRaises(ValueError): + project(data) + + def test_cancellation(self): + total = 0 + + def record(): + nonlocal total + total += 1 + + project(fixture(), check_cancel=record) + for stop in range(1, total + 1): + count = 0 + + def cancel(stop=stop): + nonlocal count + count += 1 + if count == stop: + raise InterruptedError("cancelled") + + with self.subTest(stop=stop), self.assertRaises(InterruptedError): + project(fixture(), check_cancel=cancel) From 95eb789c7766ea06e814f630feff644d3a1f0d4d Mon Sep 17 00:00:00 2001 From: ExoHayvan <18trevor3695@gmail.com> Date: Sun, 11 Oct 2026 23:45:40 -0400 Subject: [PATCH 2/3] Use exact rational evidence gates independent of Decimal context --- docs/capability-fixture.md | 3 +- src/aethermesh_core/capability_fixture.py | 8 +-- tests/test_capability_fixture.py | 60 +++++++++++++++++++++++ 3 files changed, 66 insertions(+), 5 deletions(-) diff --git a/docs/capability-fixture.md b/docs/capability-fixture.md index 8c324894..8311cb2b 100644 --- a/docs/capability-fixture.md +++ b/docs/capability-fixture.md @@ -48,7 +48,8 @@ necessary to establish that two differently named actors are independent. Metrics must match policy exactly. Conservative uncertainty intervals must satisfy resource/value bounds, every regression budget and each required positive gain. -Decimal comparison avoids binary floating-point subtraction at policy boundaries. +Exact rational comparisons of decoded numeric values avoid binary floating-point +subtraction and do not depend on ambient Decimal precision, rounding or traps. Missing/conflicting evidence or failed gates returns experimental with no proposed name or version increment. Passing evidence that does not classify the intended capability returns unclassified. Internal versions stop at 4096. No training-run diff --git a/src/aethermesh_core/capability_fixture.py b/src/aethermesh_core/capability_fixture.py index 4cf6aa56..14402e20 100644 --- a/src/aethermesh_core/capability_fixture.py +++ b/src/aethermesh_core/capability_fixture.py @@ -6,7 +6,7 @@ import json import re from dataclasses import dataclass -from decimal import Decimal +from fractions import Fraction from typing import Any, Literal from .artifact_codec import CheckCancel, _cancel, _integer @@ -75,11 +75,11 @@ def _digest(value: Any) -> str: return str(value) -def _number(value: Any) -> Decimal: +def _number(value: Any) -> Fraction: if type(value) not in (int, float): raise ValueError("fixture metric must be a finite number") - result = Decimal(str(value)) - if not result.is_finite() or abs(result) > 10**12: + result = Fraction(str(value)) + if abs(result) > 10**12: raise ValueError("fixture metric magnitude bound") return result diff --git a/tests/test_capability_fixture.py b/tests/test_capability_fixture.py index 128abcee..3ddc5847 100644 --- a/tests/test_capability_fixture.py +++ b/tests/test_capability_fixture.py @@ -1,6 +1,7 @@ """Synthetic evidence must never become publication or live capability authority.""" import copy +import decimal import json import unittest from pathlib import Path @@ -291,3 +292,62 @@ def cancel(stop=stop): with self.subTest(stop=stop), self.assertRaises(InterruptedError): evaluate(fixture(), check_cancel=cancel) + + def test_gate_decisions_ignore_ambient_decimal_context(self): + accepted = fixture() + for report in accepted["reports"].values(): + report["metrics"]["accuracy"]["uncertainty"] = 0 + accepted["reports"]["candidate"]["metrics"]["accuracy"]["value"] = 0.85 + rejected = copy.deepcopy(accepted) + rejected["reports"]["candidate"]["metrics"]["accuracy"]["value"] = 0.849 + rounded_up = fixture() + rounded_up["reports"]["candidate"]["metrics"]["accuracy"]["value"] = 0.84 + cases = [accepted, rejected, rounded_up] + for uncertainty in (1e-30, 5e-324): + tiny = copy.deepcopy(accepted) + tiny["reports"]["candidate"]["metrics"]["accuracy"]["uncertainty"] = ( + uncertainty + ) + self.assertIn("insufficient_gain", evaluate(tiny).reasons) + cases.append(tiny) + classified = copy.deepcopy(cases[3]) + classified["policy"]["metrics"][0]["gain"] = 0.04 + classified["taxonomy"]["rules"][0]["minimum"] = 0.85 + retax(classified) + self.assertEqual(evaluate(classified).status, "unclassified") + resource = copy.deepcopy(cases[3]) + resource["policy"]["metrics"][0].update(gain=0.04, maximum=0.85) + self.assertIn("resource_or_uncertainty", evaluate(resource).reasons) + regression = copy.deepcopy(accepted) + regression["reports"]["candidate"]["metrics"]["memory"].update( + value=1024, uncertainty=1e-30 + ) + regression["policy"]["metrics"][1]["uncertainty"] = 1 + self.assertIn("regression", evaluate(regression).reasons) + cases.extend((classified, resource, regression)) + expected = tuple(evaluate(case) for case in cases) + self.assertEqual(expected[0].status, "eligible") + self.assertIn("insufficient_gain", expected[1].reasons) + self.assertIn("insufficient_gain", expected[2].reasons) + for precision in (1, 2, 28, 100, 350): + for rounding in ( + decimal.ROUND_DOWN, + decimal.ROUND_UP, + decimal.ROUND_HALF_EVEN, + decimal.ROUND_FLOOR, + decimal.ROUND_CEILING, + ): + with ( + self.subTest(precision=precision, rounding=rounding), + decimal.localcontext() as context, + ): + context.prec = precision + context.rounding = rounding + context.Emax = 1 + context.Emin = -1 + for signal in context.traps: + context.traps[signal] = True + self.assertEqual(tuple(evaluate(case) for case in cases), expected) + self.assertFalse(any(context.flags.values())) + with self.assertRaises(ValueError): + naming._number(10**12 + 1) From 0389d0a7e7f646a7ba9de555a94ccba46f88ecb9 Mon Sep 17 00:00:00 2001 From: ExoHayvan <18trevor3695@gmail.com> Date: Sun, 11 Oct 2026 23:51:06 -0400 Subject: [PATCH 3/3] Reject lossy JSON numeric evidence before rational gates --- docs/capability-fixture.md | 4 ++++ src/aethermesh_core/capability_fixture.py | 18 +++++++++++++++++- tests/test_capability_fixture.py | 20 ++++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/docs/capability-fixture.md b/docs/capability-fixture.md index 8311cb2b..c19a11f4 100644 --- a/docs/capability-fixture.md +++ b/docs/capability-fixture.md @@ -50,6 +50,10 @@ Metrics must match policy exactly. Conservative uncertainty intervals must satis resource/value bounds, every regression budget and each required positive gain. Exact rational comparisons of decoded numeric values avoid binary floating-point subtraction and do not depend on ambient Decimal precision, rounding or traps. +Float literals must retain their exact decimal value through JSON decoding; lossy +rounding and nonzero underflow are rejected before evidence is considered. Float +literals are capped at 1,024 characters and exponent magnitude 400 before exact +conversion, preventing unbounded powers of ten. Equivalent trailing zeros are allowed. Missing/conflicting evidence or failed gates returns experimental with no proposed name or version increment. Passing evidence that does not classify the intended capability returns unclassified. Internal versions stop at 4096. No training-run diff --git a/src/aethermesh_core/capability_fixture.py b/src/aethermesh_core/capability_fixture.py index 14402e20..58fc8256 100644 --- a/src/aethermesh_core/capability_fixture.py +++ b/src/aethermesh_core/capability_fixture.py @@ -4,6 +4,7 @@ import hashlib import json +import math import re from dataclasses import dataclass from fractions import Fraction @@ -40,12 +41,27 @@ def fixture_digest(value: Any) -> str: return hashlib.sha256(raw).hexdigest() +def _float_literal(token: str) -> float: + # Bound exact conversion before constructing powers of ten. Reject JSON + # parsing loss too: a rounded threshold or erased uncertainty is not evidence. + parts = re.split("[eE]", token) + if len(token) > 1024 or (len(parts) == 2 and abs(int(parts[1])) > 400): + raise ValueError("fixture numeric literal bound") + value = float(token) + if not math.isfinite(value) or Fraction(token) != Fraction(str(value)): + raise ValueError("lossy fixture numeric literal") + return value + + def _decode(raw: bytes) -> dict[str, Any]: if type(raw) is not bytes or not 0 < len(raw) <= MAX_FIXTURE_BYTES: raise ValueError("fixture input exceeds byte bound") try: value = json.loads( - raw.decode("utf-8"), object_pairs_hook=_object, parse_constant=_constant + raw.decode("utf-8"), + object_pairs_hook=_object, + parse_constant=_constant, + parse_float=_float_literal, ) _validate_message(value) except (ValueError, RecursionError, ProtocolError) as error: diff --git a/tests/test_capability_fixture.py b/tests/test_capability_fixture.py index 3ddc5847..5fafe16f 100644 --- a/tests/test_capability_fixture.py +++ b/tests/test_capability_fixture.py @@ -351,3 +351,23 @@ def test_gate_decisions_ignore_ambient_decimal_context(self): self.assertFalse(any(context.flags.values())) with self.assertRaises(ValueError): naming._number(10**12 + 1) + + def test_json_numeric_decoding_cannot_erase_uncertainty_or_round_a_gain(self): + data = fixture() + data["reports"]["baseline"]["metrics"]["accuracy"]["uncertainty"] = 0 + data["reports"]["candidate"]["metrics"]["accuracy"].update( + value=0.85, uncertainty=1e-30 + ) + raw = json.dumps(data).encode() + for malformed in ( + raw.replace(b"1e-30", b"1e-400"), + raw.replace(b"0.85", b"0.849999999999999999999999999999"), + raw.replace(b"1e-30", b"0e99999999999999999999999999999"), + raw.replace(b"1e-30", b"0." + b"0" * 1024 + b"1"), + ): + with self.subTest(raw=malformed[-100:]), self.assertRaises(ValueError): + naming.evaluate_naming_fixture(malformed, **pins(data)) + exact = raw.replace(b"0.85", b"0.850000000000000000000000000000") + self.assertEqual( + naming.evaluate_naming_fixture(exact, **pins(data)), evaluate(data) + )