diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5ceed1e..a2373f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,7 +39,7 @@ jobs: - name: Install package and development tools run: | - python -m pip install --upgrade pip + python -m pip install --upgrade pip "setuptools>=69" python -m pip install -e ".[dev]" - name: Check formatting diff --git a/README.md b/README.md index e68e68c..99cd772 100644 --- a/README.md +++ b/README.md @@ -1,248 +1,316 @@ # IX-Sally -IX-Sally is a source-available, evaluation-only governed AI operating architecture for controlled proposal intake, evidence handling, staged execution, human review, certified resume, audited reentry, and closeout reporting. - -The core rule is simple: - -**AI proposes. Humans decide. Evidence governs what may proceed.** - -IX-Sally is not an autonomous agent framework, not a deployment bot, not a compliance certification engine, and not an AGI claim. It is a receipt-driven control-plane architecture for proving that proposals, execution readiness, human decisions, reentry, and closeout status were handled under explicit authority boundaries. - -## Purpose - -Modern AI systems can produce confident outputs that look actionable before they are grounded, reviewed, authorized, or safe to execute. IX-Sally is built around the opposite assumption: +IX-Sally is a source-available, evaluation-only governed experimental cognitive +runtime. It combines a deterministic human-authority control plane with typed +IX language execution, bounded memory, world modeling, planning, learning, +metacognition, persistence, and evidence-linked proposal generation. + +The controlling rule is: + +> **AI proposes. Humans decide. Evidence governs what may proceed.** + +IX-Sally is not a demonstrated AGI, autonomous deployment platform, +certification authority, or substitute for independent engineering and human +judgment. + +## What is delivered + +The repository contains two integrated architectural layers. + +### Governed control plane + +The inherited IX-Sally control plane provides: + +- deterministic canonical records and SHA-256 digest links; +- doctrine, claims, jurisdiction, contracts, and bounded run state; +- proposal intake and proposal gateways; +- evidence records, support findings, and evidence processing; +- stage readiness, stage gates, orchestration, and advance receipts; +- execution planning, queues, dispatch, and Forge result processing; +- human-review handoffs, dockets, packets, bundles, and decisions; +- clearance, resume certification, reentry, audit, and complete reentry; +- closeout reports, ledgers, coordination records, and export packets; +- explicit separation between capability, evidence, permission, and authority. + +### Experimental cognitive runtime + +The cognitive runtime adds: + +- complete IX lexical, syntactic, semantic, and type analysis; +- exact typed cognitive scalar values; +- deterministic bytecode compilation; +- a bounded step-limited virtual machine; +- immutable execution status, failure, output, memory, and trace receipts; +- atomic VM memory commitment after a clean halt; +- grounded closed-catalog primitives without dynamic Python callbacks; +- a bounded typed attention workspace; +- working, episodic, semantic, and procedural memory; +- explicit pending, verified, stale, contradicted, and quarantined status; +- replayable digest-linked cognitive episodes; +- observed, inferred, predicted, hypothetical, and contradicted world facts; +- evidence-bound causal inference, prediction, and counterfactual simulation; +- dependency-aware goals and bounded deterministic planning; +- exact preconditions, effects, cost, risk, and authority metadata; +- calibrated uncertainty, Brier score, and calibration-error reporting; +- training, validation, and held-out curriculum splits; +- evidence-bound learning, retention, and transfer records; +- an evidence-limited self model and regression-aware adaptation proposals; +- a functional ninefold cognitive cycle using IX-Sally's canonical roles; +- complete canonical snapshots, exact restoration, backup, and recovery; +- an explicit bridge from cognitive plans into the existing proposal path; +- human-authority and risk gates before consequential action may proceed. + +## Core boundaries + +IX-Sally preserves these distinctions throughout the implementation: - Output is not evidence. -- Proposal is not permission. +- Memory is not truth. +- Prediction is not observation. +- Simulation is not execution. - Capability is not authority. -- Human review is not optional ceremony. -- Resume after human review must be certified. -- Reentry after resume must be audited. -- Closeout must be ledgered and exportable. - -The system is designed to make every meaningful transition inspectable through deterministic records, digests, receipts, ledgers, reports, and control-plane snapshots. - -## What IX-Sally does - -IX-Sally models a governed workflow for AI-assisted operations: - -1. Establish a bounded contract and run state. -2. Accept proposals only through explicit intake and gateway structures. -3. Evaluate evidence and support before action is treated as ready. -4. Gate staged orchestration through readiness checks. -5. Dispatch only bounded execution candidates. -6. Process Forge results through structured result gateways. -7. Route human-boundary actions into human review. -8. Record human review handoffs, packets, dockets, bundles, and decisions. -9. Certify cleared resume after human authority is recorded. -10. Reenter orchestration after resume with explicit receipts. -11. Audit reentry before treating it as accepted. -12. Record complete reentry and closeout. -13. Export closeout evidence packets for downstream review. - -The emphasis is not on making an AI act faster. The emphasis is on making it harder for unearned authority, unsupported claims, or skipped review to pass unnoticed. - -## Current capability - -IX-Sally now includes a complete governed control-plane path from proposal intake through final human-review reentry closeout export. - -Major capabilities include: - -- Deterministic digest records and canonical payloads. -- Doctrine and boundary primitives. -- Runtime contracts and bounded run state. -- Proposal intake, proposal gateway, and Sally proposal routing. -- Evidence records, evidence support records, and evidence support processing. -- Stage readiness, stage gates, stage advance receipts, and staged orchestration. -- One-step orchestration and loop-runner support. -- Execution queues, execution planning, execution dispatch, and Forge result processing. -- Forge evidence, Forge result records, Forge result gateway, and Forge result processing. -- Human-review gateway, docket, packets, operator bundle, and bundle ledger. -- Human-review handoff coordination. -- Human-review decision ledger and decision coordination. -- Human-review resolution audit and clearance reporting. -- Human-review resume certificate, resume ledger, and resume coordination. -- Human-review control-plane state, coordinator, status, snapshot, and report. -- Human-review workflow kit for handoff, decision, clearance, resume, reentry, audit, complete reentry, and closeout. -- Certified human-review reentry runner. -- Reentry result ledger. -- Reentry audit report and audit ledger. -- Audited reentry coordination and ledger. -- Complete reentry coordination and ledger. -- Complete reentry closeout report and closeout ledger. -- Complete reentry closeout coordination and coordination ledger. -- Complete reentry closeout export packet. +- A proposal is not permission. +- A test result is not certification. +- Repetition does not convert a claim into truth. +- IX-Sally may not approve its own consequential action. +- IX-Sally may not certify itself as AGI. + +## Architecture flow + +```text +IX source + -> lexer / parser / semantic validation / type checking + -> deterministic bytecode compiler + -> bounded virtual machine + -> typed output, memory, failure, and trace receipt + +observations and admitted evidence + -> cognitive workspace and active memory + -> epistemically typed world model + -> causal inference, prediction, and counterfactuals + -> goal graph and bounded planner + -> executive uncertainty, risk, and authority gates + -> cognitive proposal bridge + -> existing IX-Sally proposal and human-review control plane + +observed outcomes + -> learning ledger and capability measures + -> curriculum, retention, and held-out transfer records + -> regression-aware adaptation proposal + -> separate human decision and validation boundary +``` -## Human-review control plane +Detailed design documentation is available in: + +- `docs/ARCHITECTURE.md` +- `docs/USAGE.md` +- `docs/AGI_CLAIM_BOUNDARY.md` +- `docs/ROADMAP_COMPLETION_LEDGER.md` +- `VALIDATION_REPORT.md` + +## Repository layout + +```text +IX-Sally/ +├── .github/workflows/ GitHub Actions quality gates +├── docs/ Architecture, usage, boundaries, and ledger +├── examples/ Executable IX and Python demonstrations +├── src/ix_sally/ +│ ├── cognition/ Integrated cognitive runtime +│ ├── language/ IX language frontend and type system +│ └── ... Governed control-plane modules +├── tests/ Complete source test inventory +├── check_green.py Unified quality-gate runner +├── repository_check.py Repository-integrity gate +├── dependency_check.py Dependency-cycle gate +├── architecture_check.py Runtime-boundary gate +├── package_smoke.py Installed-wheel verification +├── pyproject.toml Package and tool configuration +├── VALIDATION_REPORT.md Exact local validation evidence and limits +└── LICENSE Controlling evaluation-only terms +``` -The human-review control plane is the center of the repository. +## Requirements -It records and reports: +- Python 3.11, 3.12, or 3.13 +- No declared third-party runtime dependencies -- Handoffs. -- Human decisions. -- Resume certificates. -- Reentry results. -- Reentry audits. -- Audited reentries. -- Complete reentries. -- Complete reentry closeouts. -- Complete reentry closeout coordination results. +Development tools are available through the `dev` extra: -The control plane is intentionally ledger-driven. Each update creates a new immutable state instead of mutating prior records. This gives the runtime a stable basis for asking: +```text +python -m pip install -e ".[dev]" +``` -- What was handed off? -- Who decided? -- Was resume cleared? -- Did reentry actually advance state? -- Did the reentry audit pass? -- Was the complete reentry recorded? -- Was closeout accepted, waiting, or blocked? -- Can the result be exported without operator attention? +## Run the complete quality gate -## Complete reentry closeout path +```text +python check_green.py +``` -The final closeout path is the most complete human-review lifecycle currently represented in IX-Sally. +Individual gates can be selected: + +```text +python check_green.py --gate format +python check_green.py --gate lint +python check_green.py --gate type-check +python check_green.py --gate repository +python check_green.py --gate dependencies +python check_green.py --gate architecture +python check_green.py --gate test +python check_green.py --gate package +``` -A cleared resume can be passed through: +## Run the observed cognitive evaluation -1. Complete reentry coordination. -2. Reentry execution. -3. Reentry audit. -4. Audited reentry recording. -5. Complete reentry recording. -6. Closeout report creation. -7. Closeout workflow recording. -8. Closeout coordination ledgering. -9. Export packet creation. +```text +python -m ix_sally --cognitive-evaluation +``` -The export packet links the major evidence layers: +The command emits canonical JSON for fifteen deterministic local benchmarks: + +1. typed IX arithmetic; +2. governed VM memory; +3. grounded primitive execution; +4. active-memory retrieval; +5. causal prediction; +6. bounded planning; +7. measured transfer; +8. functional ninefold coordination; +9. human-authority blocking; +10. calibrated uncertainty; +11. executive-to-governance bridging; +12. separated held-out curriculum evidence; +13. replayable episode chaining; +14. regression-aware adaptation; +15. exact complete-state restoration. + +A passing report still contains: + +```text +classification = experimental-cognitive-architecture +agi_certified = false +``` -- Coordination result. -- Coordination receipt. -- Closeout report. -- Closeout workflow operation. -- Final state. -- Final control plane. -- Closeout ledger. -- Closeout coordination ledger. +## Execute IX source -This makes the final state portable for review without implying automatic approval, certification, or production readiness. +Run the included example: -## Repository shape +```text +python -m ix_sally --execute-ix examples/answer.ix +``` -IX-Sally is a standalone modular-monolith Python repository. It is intentionally not a collection of repo-hopping integrations. The modules are small, explicit, testable units connected through typed records and deterministic payloads. +Example IX program: -Core module families include: +```text +let answer = 6 * 7 +remember answer = answer +print recall answer +assert answer == 42 +``` -- Foundation, digest, state, runtime, and recording. -- Doctrine, claims, boundaries, jurisdiction, and contracts. -- Proposals, proposal intake, and proposal gateways. -- Evidence, evidence support, and evidence processing. -- Stage readiness, stage gates, orchestration, and orchestration receipts. -- Execution planning, queueing, dispatch, and Forge result processing. -- Human review handoff, packets, dockets, bundles, decisions, clearance, resume, reentry, audit, closeout, and export. +The command returns a deterministic JSON receipt with typed local values, +outputs, memories, instruction trace, status, and failure information. -## Design principles +## Python integration -IX-Sally follows these principles: +```python +from ix_sally.cognition import SallyCognitiveSystem -### 1. No self-approval +system = SallyCognitiveSystem.create() +result = system.execute_ix( + "let answer = 6 * 7\nprint answer\nassert answer == 42\n", + filename="example.ix", +) -The system may create records, evaluate readiness, and prepare evidence. It may not convert its own output into authority. +print(result.status.value) +print(result.to_payload()) +``` -### 2. Evidence before motion +## Persist and restore complete state -Readiness and support are represented as records. Unsupported claims must not silently become executable steps. +```python +from pathlib import Path -### 3. Human authority remains explicit +from ix_sally.cognition import SallyCognitiveSystem, SnapshotRepository -Human-review boundaries are not hidden inside generic success paths. Handoffs, decisions, clearance, resume, and reentry are separate records. +system = SallyCognitiveSystem.create() +system.execute_ix("remember answer = 42\n", filename="memory.ix") -### 4. Resume is certified +repository = SnapshotRepository(Path("ix-sally-state.json")) +repository.save(system.snapshot()) +loaded = repository.load() +restored = SallyCognitiveSystem.from_snapshot(loaded.snapshot) -After human review, the system does not simply continue. It records a resume certificate and resumes from a traceable state. +assert restored.state_payload() == system.state_payload() +``` -### 5. Reentry is audited +Snapshot restoration validates canonical payloads and state digests before +reconstructing the integrated runtime. Backup recovery fails closed when no +valid copy remains. -A resumed workflow is not treated as clean merely because it advanced. Reentry is separately audited and reported. +## Local validation evidence -### 6. Closeout is exportable +The final source state was locally observed with: -The final closeout state can be converted into a digest-linked export packet for inspection, review, or downstream evidence handling. +- CPython 3.13.5 compilation across `src`, `tests`, and `examples`; +- exactly 969 collected tests across 135 test files; +- all 969 tests passing in four balanced shards; +- repository integrity passing with 0 violations; +- 123 runtime modules and 655 imports with 0 dependency cycles; +- 0 architecture-boundary violations; +- installed-wheel smoke testing passing; +- all 15 built-in cognitive benchmarks passing; +- 0 Python lines over the configured 100-character limit; +- 0 trailing-whitespace findings; +- 0 source TODO/FIXME/placeholder/`pass`/`NotImplementedError` findings. -### 7. Determinism over theater +Ruff, Mypy, CPython 3.11, and CPython 3.12 were not available in the local +execution environment and are not falsely reported as passed. GitHub Actions is +configured to run formatting, lint, strict typing, structural checks, tests, +and wheel verification on Python 3.11, 3.12, and 3.13. -The repository favors small immutable records, stable JSON-compatible payloads, digest links, and tests over broad claims. +See `VALIDATION_REPORT.md` for the exact evidence and limitations. -## What IX-Sally is not +## AGI claim boundary -IX-Sally is not: +IX-Sally implements mechanisms commonly explored in cognitive architectures, +but architecture completion and local tests do not establish artificial +general intelligence. -- An AGI system. -- An autonomous operations platform. -- A production deployment tool. -- A certification authority. -- A substitute for human judgment. -- A legal, safety, security, or compliance guarantee. -- A system that grants AI independent authority. +This repository does not prove: -It is an evaluation architecture for governed AI workflow control. +- human-level or open-world general intelligence; +- broad unfamiliar-task competence; +- autonomous scientific discovery; +- safe recursive self-improvement; +- robust embodiment; +- production safety; +- certification or regulatory compliance. -## Example use cases +No external foundation-model weights, training corpus, sensor stream, robotics +stack, or proprietary benchmark data is bundled. Those are explicit external +system boundaries rather than hidden missing files. -IX-Sally may be useful for research and evaluation work involving: +The accurate description is: -- AI proposal governance. -- Human-in-the-loop execution control. -- AI evidence handling. -- Audit-oriented orchestration. -- Review-gated tool execution. -- Safety-case style workflow records. -- Receipt-driven agent evaluation. -- Control-plane design for AI-assisted engineering. -- Demonstrations of bounded AI authority. +> IX-Sally is a governed experimental cognitive runtime and research +> architecture. It is not a demonstrated AGI. ## Development status -This repository is a research and evaluation build. It is structured around deterministic behavior, typed records, and tests. Interfaces may evolve as the control-plane model is refined. - -Do not treat this repository as production-ready. Do not use it to authorize real-world execution without independent engineering, legal, security, safety, and human governance review. - -## Running tests - -Typical local development flow: - -``` -python -m pytest -python -m ruff check . -python -m ruff format --check . -python -m mypy src tests -``` -Depending on environment configuration, set PYTHONPATH=src before running tests. - -PowerShell example: -``` -$env:PYTHONPATH="src" -python -m pytest -python -m ruff check . -python -m ruff format --check . -python -m mypy src tests -``` -License posture - -IX-Sally is source-available for evaluation and review. - -Public viewing and evaluation are allowed. Production use, commercial use, hosted use, derivative use, funded use, government or regulated operational use, redistribution, or ownership transfer require written permission and a paid commercial license from Bryce Lovell. - -See the repository license file for the controlling terms. - -Claim boundary +IX-Sally remains an alpha research and evaluation build. It is intended for +architecture review, deterministic experimentation, governed-agent research, +human-authority workflow evaluation, and reproducible cognitive-runtime tests. -IX-Sally demonstrates a governed control-plane architecture for AI-assisted proposal, evidence, review, resume, reentry, audit, closeout, and export workflows. +Do not use it to authorize real-world consequential execution without separate +engineering, security, legal, safety, operational, and human governance review. -It does not claim autonomous trustworthiness. It does not claim certification. It does not claim AGI. +## License -The correct interpretation is: +IX-Sally is source-available for evaluation and review under the terms in +`LICENSE`. -IX-Sally is a receipt-driven governed AI workflow architecture where AI output remains subordinate to evidence, explicit authority, human review, and auditable control-plane state. +Production use, commercial use, hosted use, derivative use, funded use, +government or regulated operational use, redistribution, or ownership transfer +requires prior written permission and a paid commercial license from Bryce +Lovell. diff --git a/VALIDATION_REPORT.md b/VALIDATION_REPORT.md new file mode 100644 index 0000000..c1c5e07 --- /dev/null +++ b/VALIDATION_REPORT.md @@ -0,0 +1,141 @@ +# IX-Sally Validation Report + +## Release identity + +- Repository: `IX-Sally` +- Package: `ix-sally` +- Package version: `0.1.0` +- Delivery model: one complete source repository state +- Runtime dependency declaration: zero third-party packages +- Local validation interpreter: CPython 3.13.5 + +## Scope of this shipment + +This repository combines the inherited IX-Sally governance control plane with a +new bounded experimental cognitive runtime. The delivered implementation +includes typed IX execution, deterministic bytecode, a bounded virtual machine, +grounded primitives, attention, active memory, replayable episodes, an +epistemically typed world model, causal inference, planning, goal selection, +calibrated uncertainty, curricula, learning, held-out transfer records, +metacognition, regression-aware adaptation, persistence, recovery, a functional +ninefold cycle, and integration into the existing human-authority proposal path. + +This is a complete source shipment for those declared capabilities. It contains +no known TODO-only implementations, `pass` placeholders, `NotImplementedError` +paths, or intentionally empty subsystem stubs. + +External foundation-model weights, training corpora, sensors, actuators, +network services, and proprietary benchmark data are not bundled. They are +explicit external system boundaries, not silently missing repository files. + +## Baseline repair + +The supplied source archive contained fourteen malformed indentation prefixes +across twelve source and test files. Those syntax defects were repaired without +changing the intended behavior. After repair, the inherited test baseline +passed all 889 collected tests. + +## Final observed test inventory + +Pytest collection after the cognitive implementation and release hardening +identified exactly **969 tests across 135 test files**. + +The inventory was divided into four balanced deterministic file shards to avoid +allowing one long process to hide a slow tail: + +| Shard | Tests | Result | +| --- | ---: | --- | +| 1 | 243 | Passed, process exit 0 | +| 2 | 242 | Passed, process exit 0 | +| 3 | 242 | Passed, process exit 0 | +| 4 | 242 | Passed, process exit 0 | +| **Total** | **969** | **Passed** | + +No test failure, collection error, or skipped failure was observed in those +runs. + +## Executed quality gates + +The following gates were executed against the final working tree: + +| Gate | Observed result | +| --- | --- | +| Python compilation | `src`, `tests`, and `examples` compiled successfully | +| Repository integrity | 263 clean Python source/test files, 0 violations | +| Runtime dependency graph | 123 modules, 655 imports, 0 cycles | +| Runtime architecture | 123 modules, 655 imports, 0 boundary violations | +| Installed wheel smoke test | Passed | +| Built-in cognitive evaluation | 15 of 15 observed benchmarks passed | +| IX example execution | Halted cleanly with a deterministic execution receipt | +| Empty complete snapshot | Canonical snapshot produced and validated | +| Integrated cognitive example | Completed and produced proposal/receipt output | +| Python source line limit | 0 lines over 100 characters in `src`, `tests`, and `examples` | +| Trailing whitespace | 0 findings in source, tests, examples, docs, and configuration | +| Incomplete implementation scan | 0 source TODO/FIXME/placeholder/`pass`/`NotImplementedError` findings | + +The only occurrence of the word `placeholder` is in a parser test asserting +that blank lines and comments do not create placeholder statements. + +## Built-in cognitive evaluation + +`python -m ix_sally --cognitive-evaluation` observed all fifteen declared local +benchmarks passing: + +1. typed IX arithmetic; +2. governed VM memory; +3. grounded primitive execution; +4. active-memory retrieval; +5. causal prediction; +6. bounded planning; +7. measured transfer; +8. functional ninefold coordination; +9. human-authority blocking; +10. calibrated uncertainty; +11. executive-to-governance bridging; +12. separated held-out curriculum evidence; +13. replayable episode chaining; +14. regression-aware adaptation; +15. exact complete-state restoration. + +The report classifies the repository as an +`experimental-cognitive-architecture` and fixes `agi_certified` to `false`. +Passing these checks verifies implemented mechanisms; it does not establish +artificial general intelligence. + +## Gates not executed locally + +The following configured CI gates could not be executed in the available local +environment: + +- Ruff formatting check; +- Ruff lint check; +- Mypy strict type check; +- CPython 3.11 execution; +- CPython 3.12 execution. + +The environment contained CPython 3.13.5 and Pytest, but its package source did +not provide Ruff or Mypy, and Python 3.11/3.12 interpreters were not installed. +Those tools were not simulated and their results are not claimed. + +The repository retains GitHub Actions configuration for Python 3.11, 3.12, and +3.13. That workflow installs the development tools and runs formatting, lint, +strict typing, repository, dependency, architecture, test, and installed-wheel +gates. A GitHub green status remains external evidence to obtain after upload; +this report does not fabricate it. + +## Claim boundary + +This shipment establishes a substantial governed experimental cognitive +runtime. It does not establish: + +- AGI; +- human-level intelligence; +- open-world competence; +- production safety; +- autonomous authority; +- certification; +- legal, security, or regulatory compliance. + +The controlling operating rule remains: + +> AI proposes. Humans decide. Evidence governs what may proceed. diff --git a/architecture_check.py b/architecture_check.py index 82bbe51..5e8c979 100644 --- a/architecture_check.py +++ b/architecture_check.py @@ -27,10 +27,7 @@ class ArchitectureViolation: def render(self) -> str: """Return a stable human-readable violation message.""" - return ( - f"{self.rule}: {self.source} imports {self.target} " - f"at line {self.line}" - ) + return f"{self.rule}: {self.source} imports {self.target} at line {self.line}" def _is_status_module(module: str) -> bool: @@ -66,9 +63,8 @@ def architecture_violations( ) ) - if ( - dependency.target.startswith(_HUMAN_REVIEW_PREFIX) - and not dependency.source.startswith(_HUMAN_REVIEW_PREFIX) + if dependency.target.startswith(_HUMAN_REVIEW_PREFIX) and not dependency.source.startswith( + _HUMAN_REVIEW_PREFIX ): violations.append( ArchitectureViolation( diff --git a/check_green.py b/check_green.py index 0bc3486..142c93c 100644 --- a/check_green.py +++ b/check_green.py @@ -76,9 +76,7 @@ def command(self) -> tuple[str, ...]: ), ) -_GATE_BY_NAME: Final[dict[str, QualityGate]] = { - gate.name: gate for gate in QUALITY_GATES -} +_GATE_BY_NAME: Final[dict[str, QualityGate]] = {gate.name: gate for gate in QUALITY_GATES} def _parser() -> argparse.ArgumentParser: @@ -135,9 +133,7 @@ def main(argv: Sequence[str] | None = None) -> int: failures.append(gate.name) if failures: - sys.stderr.write( - f"Failed quality gates: {', '.join(failures)}\n" - ) + sys.stderr.write(f"Failed quality gates: {', '.join(failures)}\n") return 1 sys.stdout.write("All selected quality gates passed.\n") diff --git a/dependency_check.py b/dependency_check.py index f03f19f..56a3ba9 100644 --- a/dependency_check.py +++ b/dependency_check.py @@ -31,17 +31,11 @@ class DependencyGraph: def adjacency(self) -> dict[str, tuple[str, ...]]: """Return deterministic adjacency entries for every known module.""" - targets: dict[str, set[str]] = { - module: set() - for module in self.modules - } + targets: dict[str, set[str]] = {module: set() for module in self.modules} for dependency in self.dependencies: targets[dependency.source].add(dependency.target) - return { - module: tuple(sorted(module_targets)) - for module, module_targets in targets.items() - } + return {module: tuple(sorted(module_targets)) for module, module_targets in targets.items()} def cycles(self) -> tuple[tuple[str, ...], ...]: """Return deterministic strongly connected runtime dependency cycles.""" @@ -95,10 +89,7 @@ def visit(module: str) -> None: component for component in components if len(component) > 1 - or ( - len(component) == 1 - and component[0] in adjacency[component[0]] - ) + or (len(component) == 1 and component[0] in adjacency[component[0]]) ] return tuple(sorted(cyclic_components)) @@ -249,10 +240,7 @@ def visit_statements( ) -> None: for statement in statements: if isinstance(statement, ast.If): - guarded = ( - type_checking_only - or _is_type_checking_guard(statement.test) - ) + guarded = type_checking_only or _is_type_checking_guard(statement.test) visit_statements( statement.body, type_checking_only=guarded, @@ -263,10 +251,7 @@ def visit_statements( ) continue - if ( - isinstance(statement, ast.ImportFrom) - and not type_checking_only - ): + if isinstance(statement, ast.ImportFrom) and not type_checking_only: for target in _from_import_targets( statement, source_module=source_module, @@ -276,17 +261,14 @@ def visit_statements( imports.add((target, statement.lineno)) continue - if ( - isinstance(statement, ast.Import) - and not type_checking_only - ): + if isinstance(statement, ast.Import) and not type_checking_only: for alias in statement.names: - target = _nearest_known_module( + resolved_target = _nearest_known_module( alias.name, known_modules=known_modules, ) - if target is not None: - imports.add((target, statement.lineno)) + if resolved_target is not None: + imports.add((resolved_target, statement.lineno)) continue nested_statements: list[ast.stmt] = [] @@ -392,17 +374,10 @@ def _cycle_message( ) -> str: """Return a readable failure message for dependency cycles.""" dependency_lines: dict[tuple[str, str], list[int]] = {} - cycle_modules = { - module - for cycle in cycles - for module in cycle - } + cycle_modules = {module for cycle in cycles for module in cycle} for dependency in dependencies: - if ( - dependency.source in cycle_modules - and dependency.target in cycle_modules - ): + if dependency.source in cycle_modules and dependency.target in cycle_modules: pair = ( dependency.source, dependency.target, @@ -428,14 +403,8 @@ def _cycle_message( ) ) if locations: - rendered = ", ".join( - str(line) - for line in sorted(locations) - ) - lines.append( - f" {source} imports {target} " - f"at line(s) {rendered}" - ) + rendered = ", ".join(str(line) for line in sorted(locations)) + lines.append(f" {source} imports {target} at line(s) {rendered}") return "\n".join(lines) @@ -449,9 +418,7 @@ def main() -> int: cycles = graph.cycles() if cycles: - sys.stderr.write( - f"{_cycle_message(cycles, graph.dependencies)}\n" - ) + sys.stderr.write(f"{_cycle_message(cycles, graph.dependencies)}\n") return 1 sys.stdout.write( diff --git a/docs/AGI_CLAIM_BOUNDARY.md b/docs/AGI_CLAIM_BOUNDARY.md new file mode 100644 index 0000000..5232ed7 --- /dev/null +++ b/docs/AGI_CLAIM_BOUNDARY.md @@ -0,0 +1,66 @@ +# AGI Claim Boundary + +## What this repository establishes + +IX-Sally implements and tests a coherent experimental cognitive architecture +with typed execution, grounded primitives, bounded attention, active memory, +world modeling, causal inference, prediction, planning, goal selection, +uncertainty calibration, curricula, held-out transfer records, metacognition, +regression-aware adaptation, replayable episodes, persistence, and governance. + +The repository also integrates those functions with a mature human-authority +control plane rather than allowing cognition to grant itself execution rights. + +## What this repository does not establish + +Completion of the software architecture does not establish artificial general +intelligence. The repository does not prove: + +- open-world general intelligence; +- human-level intelligence; +- autonomous scientific discovery; +- robust embodiment; +- broad real-world transfer; +- safe self-improvement; +- production safety; +- legal or regulatory compliance; +- certification of any kind. + +No external foundation-model weights, training corpus, sensor stream, robotics +stack, or proprietary benchmark dataset is bundled. That is not a missing file +inside the implementation; it is an explicit system boundary. + +## Why the built-in evaluation is not an AGI test + +The built-in suite is deterministic and local. It verifies that implemented +mechanisms behave as specified. It does not measure the breadth, efficiency, +novelty, autonomy, or environmental robustness required for an AGI claim. + +The evaluation report therefore fixes: + +```text +classification = experimental-cognitive-architecture +agi_certified = false +``` + +Attempting to construct a report with `agi_certified = true` raises an error. + +## Evidence required before stronger claims + +Any future stronger claim would require external, reproducible evidence such as: + +- broad unfamiliar-task benchmarks; +- held-out task families that were not used during implementation; +- ablation studies; +- baseline comparisons; +- long-horizon retention studies; +- adversarial evaluation; +- independent replication; +- measured resource efficiency; +- real-world or high-fidelity environment interaction; +- documented failure cases and confidence calibration. + +Until such evidence exists, the accurate description is: + +> IX-Sally is a governed experimental cognitive runtime and research +> architecture. It is not a demonstrated AGI. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..d6fc4b4 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,298 @@ +# IX-Sally Architecture + +## System identity + +IX-Sally is one standalone Python modular monolith. The repository combines two +connected systems: + +1. A deterministic, receipt-driven governance control plane for proposal, + evidence, execution, human review, resume, audited reentry, and closeout. +2. A bounded experimental cognitive runtime for typed language execution, + attention, memory, world modeling, planning, learning, metacognition, and + proposal generation. + +The cognitive runtime does not bypass the original control plane. A cognitive +plan enters the existing proposal path through an explicit bridge. Execution, +permission, evidence admission, and human authority remain separate concerns. + +## Governing invariants + +The architecture is built around these non-negotiable boundaries: + +- Output is not evidence. +- Memory is not truth. +- Capability is not authority. +- A proposal is not permission. +- A simulation is not an observed outcome. +- A test result is not certification. +- IX-Sally may not approve its own consequential action. +- IX-Sally may not certify itself as AGI. + +## Data-flow overview + +```text +IX source + -> lexer / parser / semantic validation + -> typed bytecode compiler + -> bounded virtual machine + -> execution receipt and trace + +observations / evidence + -> workspace and active memory + -> epistemically typed world model + -> causal inference and prediction + -> bounded deterministic planner + -> executive risk / uncertainty / authority gates + -> cognitive proposal bridge + -> existing IX-Sally proposal and human-authority control plane + +outcomes + -> learning ledger + -> capability measures + -> curriculum and held-out transfer records + -> regression-aware adaptation proposal + -> human decision and validation boundary +``` + +## Language and execution + +The `ix_sally.language` package provides source spans, diagnostics, tokens, a +lexer, expression and statement ASTs, parsing, semantic validation, type +inference, and program type checking. + +The `ix_sally.cognition` execution layer adds: + +- `CognitiveValue`: exact scalar values with explicit type identity. +- `Instruction` and `BytecodeProgram`: deterministic typed instructions. +- `IXCompiler`: compilation from validated IX AST to bounded bytecode. +- `IXVirtualMachine`: step-limited execution with no arbitrary Python callback. +- `VMResult`: immutable status, output, memory, failure, and trace receipt. + +VM memory is committed to the integrated system only after a clean halt. A +failed assertion or runtime failure cannot partially overwrite committed +runtime memory. + +## Grounded primitives + +`PrimitiveRegistry` contains a closed catalog of validated primitive +operations. Each primitive records: + +- canonical identity; +- arity; +- operation kind; +- lifecycle status; +- grounding evidence; +- validation evidence; +- an explicit reason when unavailable or rejected. + +`PrimitiveExecutor` dispatches only enumerated operations. It does not accept +runtime-supplied Python callables, dynamic imports, or arbitrary code strings. + +## Cognitive workspace + +`CognitiveWorkspace` is a bounded immutable attention surface. Workspace items +have explicit kinds, confidence, salience, status, provenance, and parent +links. Admission is deterministic and capacity-limited. + +Beliefs, hypotheses, goals, risks, observations, and decisions are represented +as different item kinds rather than being collapsed into untyped text. + +## Active memory + +`ActiveMemoryStore` separates four memory layers: + +- working; +- episodic; +- semantic; +- procedural. + +Every entry has a truth status, confidence, sequence number, evidence digests, +source links, tags, and an optional reason. Retrieval uses a transparent lexical +score. Truth-only retrieval excludes pending, stale, contradicted, and +quarantined entries. + +Consolidation never converts repetition into truth. Evidence and status remain +required. + +## Replayable episodes + +`EpisodeLedger` is an append-only chain of complete cognitive episodes. Each +episode records: + +- initial and final state digests; +- ordered steps; +- input and output evidence links; +- completion, block, failure, or skip status; +- the previous episode digest. + +Sequence numbers and previous-digest links are validated before admission. + +## World model + +`WorldModel` distinguishes: + +- observed facts; +- inferred facts; +- predicted facts; +- hypothetical facts; +- contradicted facts. + +`CausalRule` requires evidence and preserves the epistemic status of derived +facts. Prediction and counterfactual simulation do not rewrite predictions or +hypotheses as observations. + +## Goals and planning + +`GoalGraph` provides dependency-aware goal selection with explicit lifecycle, +priority, utility, risk limit, authority metadata, and desired world state. +Dependency cycles and missing prerequisites fail closed. + +`DeterministicPlanner` performs bounded exact-state search over declarative +actions. `ActionSpec` records preconditions, effects, cost, risk, and whether +human authority is required. + +`PlanSimulator` creates hypothetical result states and a permission receipt. It +does not perform external side effects. + +## Calibrated uncertainty + +`UncertaintyLedger` records forecast probabilities and observed outcomes. It +produces: + +- Brier score; +- fixed-bin calibration summaries; +- expected calibration error; +- capability-specific reports. + +An empty report remains a zero-sample report, not proof of perfect calibration. +The executive controller can block planning when observed calibration error +exceeds its declared threshold. + +## Executive control + +`ExecutiveController`: + +1. reconciles goals against the world model; +2. selects one eligible goal; +3. checks calibration; +4. asks the bounded planner for a plan; +5. checks explicit workspace risks; +6. checks the goal risk limit; +7. checks human-authority requirements; +8. emits a decision receipt. + +The possible outcomes include no goal, satisfied goal, plan ready, requires +human authority, blocked risk, blocked uncertainty, and plan not found. + +It proposes. It does not execute. + +## Governance bridge + +`CognitiveProposalBridge` converts a plan-ready executive decision into the +repository's existing `SallyProposalPacket` and `ProposalAction` records. + +The bridge preserves: + +- action identity; +- selected-goal rationale; +- supporting evidence; +- human-boundary metadata; +- decision-to-proposal digest links. + +Blocked decisions cannot enter the proposal path. + +## Learning and transfer + +`LearningLedger` records evidence-bound outcomes by skill and task family. +`SkillProfile` exposes observed performance rather than inferred competence. +`TransferEvaluation` separates familiar, novel, and retention scores. + +`CurriculumLedger` adds: + +- training, validation, and held-out task splits; +- task prerequisites; +- required capability labels; +- pass thresholds; +- observed trials; +- split scores; +- an explicit validation-to-held-out transfer gap. + +Held-out results are not merged into training results. + +## Metacognition and adaptation + +`SelfModel` contains only measured capability records with evidence and stated +limitations. It may identify the weakest measured capability. + +`AdaptationController` can propose a bounded improvement and compare complete +before/after capability measurements. It explicitly detects: + +- improvement; +- neutral change; +- regression; +- incomplete measurement. + +An adaptation proposal remains proposed until a separate human decision. +Regression or incomplete evaluation blocks validation advancement. + +## Functional ninefold + +`NinefoldCoordinator` runs the repository's nine canonical roles as distinct +cognitive functions. It does not simulate nine personalities or use unbounded +agent conversation. Each role emits one typed finding within the same bounded +cycle and shared evidence state. + +## Persistence and recovery + +`CognitiveSnapshot` serializes the complete integrated state into canonical JSON +with a SHA-256 state digest. + +`SallyCognitiveSystem.from_snapshot` reconstructs and validates every extended +subsystem, including: + +- workspace; +- memory; +- world model; +- action catalog; +- learning; +- self model; +- goals; +- calibration ledger; +- episode chain; +- curriculum; +- primitive registry; +- VM memories; +- counters. + +`SnapshotRepository` writes through a temporary file, flushes the file, uses +atomic replacement where the operating system provides it, maintains a prior +backup, and verifies saved bytes before reporting success. It does not claim +hardware-level durability that Python or the host filesystem cannot prove. + +## Evaluation + +`run_core_evaluation()` executes fifteen deterministic observed checks covering: + +1. typed IX arithmetic; +2. governed VM memory; +3. grounded primitives; +4. active-memory retrieval; +5. causal prediction; +6. bounded planning; +7. measured transfer; +8. functional ninefold coordination; +9. human-authority blocking; +10. calibrated uncertainty; +11. executive-to-governance bridging; +12. separated held-out curriculum evidence; +13. replayable episode chaining; +14. regression-aware adaptation; +15. exact complete-state restoration. + +The evaluation report contains `agi_certified: false` by construction. + +## Dependency posture + +The runtime declares zero third-party dependencies. Development gates use +Pytest, Ruff, and Mypy as optional development tools. The package targets Python +3.11, 3.12, and 3.13. diff --git a/docs/ROADMAP_COMPLETION_LEDGER.md b/docs/ROADMAP_COMPLETION_LEDGER.md new file mode 100644 index 0000000..7a4f82c --- /dev/null +++ b/docs/ROADMAP_COMPLETION_LEDGER.md @@ -0,0 +1,264 @@ +# IX-Sally Final-State Roadmap Completion Ledger + +## Delivery model + +This shipment uses the requested final-state delivery model. It does not attempt +to fabricate 330 historical Git commits from a source archive that did not +contain the original `.git` database. + +The locked roadmap order was preserved as an implementation dependency order, +but the deliverable is one complete repository state. + +## Restored baseline + +Before cognitive expansion, fourteen indentation corruptions across twelve +source and test files were repaired. The inherited 889-test baseline then +passed. Those repairs restored existing behavior rather than adding a numbered +feature. + +## IX language migration and kernel + +Delivered capabilities: + +- source positions and spans; +- structured diagnostics; +- token and keyword model; +- lexer; +- expression and statement ASTs; +- expression and statement parsing; +- semantic validation; +- type bindings and inference; +- program type checking; +- complete non-executing frontend analysis. + +Primary implementation: + +- `src/ix_sally/language/` + +## Typed cognitive virtual machine + +Delivered capabilities: + +- exact cognitive scalar types; +- deterministic bytecode; +- typed compiler; +- bounded VM execution; +- step limits; +- execution trace; +- typed output and memory receipts; +- assertion and runtime-failure receipts; +- atomic memory commit after clean halt; +- no arbitrary Python execution path. + +Primary implementation: + +- `cognition/values.py` +- `cognition/bytecode.py` +- `cognition/compiler.py` +- `cognition/vm.py` + +## Grounded primitive architecture + +Delivered capabilities: + +- primitive identities; +- closed operation catalog; +- arity and lifecycle validation; +- grounding evidence; +- validation evidence; +- deterministic execution; +- unavailable and rejected states; +- no dynamic callback registry. + +Primary implementation: + +- `cognition/primitives.py` + +## Cognitive workspace + +Delivered capabilities: + +- bounded attention capacity; +- typed item kinds; +- confidence and salience; +- lifecycle status; +- evidence and parent links; +- deterministic selection and eviction. + +Primary implementation: + +- `cognition/workspace.py` +- `cognition/goals.py` +- `cognition/executive.py` + +## Active memory + +Delivered capabilities: + +- working, episodic, semantic, and procedural layers; +- explicit truth status; +- evidence-bound entries; +- transparent retrieval; +- truth-only retrieval; +- consolidation without truth inflation; +- contradiction and quarantine handling; +- replayable digest-linked episodes. + +Primary implementation: + +- `cognition/active_memory.py` +- `cognition/episodes.py` + +## World model + +Delivered capabilities: + +- observed, inferred, predicted, hypothetical, and contradicted facts; +- evidence-bound causal rules; +- deterministic inference; +- prediction; +- counterfactual simulation; +- exact state matching; +- epistemic status preservation. + +Primary implementation: + +- `cognition/world_model.py` + +## Planning and action + +Delivered capabilities: + +- declarative actions; +- exact preconditions and effects; +- bounded deterministic search; +- cost and risk accounting; +- authority metadata; +- hypothetical plan simulation; +- permission receipts; +- no direct external side effects. + +Primary implementation: + +- `cognition/planning.py` +- `cognition/goals.py` +- `cognition/executive.py` + +## Functional ninefold cognition + +Delivered capabilities: + +- one bounded function for each canonical IX-Sally role; +- shared state and evidence; +- one finding per function; +- complete cycle receipt; +- no decorative multi-personality conversation loop. + +Primary implementation: + +- `cognition/ninefold.py` + +## Learning and transfer + +Delivered capabilities: + +- evidence-bound outcomes; +- skill profiles; +- retention scoring; +- familiar versus novel transfer records; +- training, validation, and held-out curriculum splits; +- prerequisites and thresholds; +- observed trial ledgers; +- explicit transfer gap; +- calibrated uncertainty records. + +Primary implementation: + +- `cognition/learning.py` +- `cognition/curriculum.py` +- `cognition/uncertainty.py` + +## Metacognition and controlled improvement + +Delivered capabilities: + +- evidence-bound capability measures; +- weakest measured capability selection; +- bounded improvement proposals; +- human-decision requirements; +- before/after regression comparison; +- incomplete-evaluation detection; +- regression blocking; +- no self-approval. + +Primary implementation: + +- `cognition/metacognition.py` +- `cognition/adaptation.py` + +## Governance integration + +Delivered capabilities: + +- executive risk and calibration gates; +- human-authority detection; +- cognitive decision receipts; +- conversion into the existing proposal packet; +- action identity preservation; +- decision-to-proposal provenance; +- blocked-decision rejection. + +Primary implementation: + +- `cognition/executive.py` +- `cognition/governance_bridge.py` +- existing proposal, authority, evidence, execution, and human-review modules. + +## Persistence and recovery + +Delivered capabilities: + +- canonical complete-state snapshots; +- state digest verification; +- exact restoration of every integrated subsystem; +- temporary-file write path; +- primary and backup validation; +- backup recovery; +- fail-closed behavior when all copies are invalid. + +Primary implementation: + +- `cognition/persistence.py` +- `cognition/restore.py` +- `cognition/storage.py` +- `cognition/system.py` + +## Evaluation and release hardening + +Delivered capabilities: + +- fifteen observed deterministic cognitive benchmarks; +- explicit non-AGI classification; +- CLI execution, evaluation, and snapshot paths; +- repository-integrity gate; +- dependency-cycle gate; +- architecture-boundary gate; +- package-wheel smoke test; +- Python 3.11-3.13 CI configuration; +- source and test line-length cleanup; +- complete documentation and claim boundary. + +Primary implementation: + +- `cognition/evaluation.py` +- `cli.py` +- `check_green.py` +- repository root quality-gate scripts; +- `.github/workflows/ci.yml` +- `docs/` + +## Final boundary + +This ledger records implemented architecture. It does not claim that a commit +count creates intelligence, that local tests prove AGI, or that an experimental +runtime is production-safe. diff --git a/docs/USAGE.md b/docs/USAGE.md new file mode 100644 index 0000000..8976ac0 --- /dev/null +++ b/docs/USAGE.md @@ -0,0 +1,98 @@ +# IX-Sally Usage + +## Install for local evaluation + +From the repository root: + +```text +python -m pip install -e ".[dev]" +``` + +The runtime itself has no declared third-party dependencies. The `dev` extra +installs the configured test, lint, formatting, and type-checking tools. + +## Run the complete quality gate + +```text +python check_green.py +``` + +Individual gates are available with `--gate`: + +```text +python check_green.py --gate format +python check_green.py --gate lint +python check_green.py --gate type-check +python check_green.py --gate repository +python check_green.py --gate dependencies +python check_green.py --gate architecture +python check_green.py --gate test +python check_green.py --gate package +``` + +## Run the observed cognitive evaluation + +```text +python -m ix_sally --cognitive-evaluation +``` + +The command prints canonical JSON. A successful local run returns process exit +code zero only when every observed benchmark passes. The report still states +`agi_certified: false`. + +## Execute an IX source file + +```text +python -m ix_sally --execute-ix examples/answer.ix +``` + +The output is a JSON execution receipt containing status, typed outputs, memory, +trace information, and failure details when applicable. + +## Print a clean complete snapshot + +```text +python -m ix_sally --empty-cognitive-snapshot +``` + +## Python integration + +```python +from ix_sally.cognition import SallyCognitiveSystem + +system = SallyCognitiveSystem.create() +result = system.execute_ix( + "let answer = 6 * 7\nprint answer\nassert answer == 42\n", + filename="example.ix", +) + +print(result.status.value) +print(result.to_payload()) +``` + +## Persist and recover state + +```python +from pathlib import Path + +from ix_sally.cognition import SallyCognitiveSystem, SnapshotRepository + +system = SallyCognitiveSystem.create() +system.execute_ix("remember answer = 42\n", filename="memory.ix") + +repository = SnapshotRepository(Path("ix-sally-state.json")) +repository.save(system.snapshot()) +loaded = repository.load() +restored = SallyCognitiveSystem.from_snapshot(loaded.snapshot) + +assert restored.state_payload() == system.state_payload() +``` + +## Run the integrated example + +```text +python examples/run_cognitive_demo.py +``` + +When running directly from an uninstalled checkout, either install the package +in editable mode or set `PYTHONPATH=src` for that process. diff --git a/examples/answer.ix b/examples/answer.ix new file mode 100644 index 0000000..e39a2b7 --- /dev/null +++ b/examples/answer.ix @@ -0,0 +1,4 @@ +let answer = 6 * 7 +remember canonical_answer = answer +recall canonical_answer +assert answer == 42 diff --git a/examples/run_cognitive_demo.py b/examples/run_cognitive_demo.py new file mode 100644 index 0000000..70e1c15 --- /dev/null +++ b/examples/run_cognitive_demo.py @@ -0,0 +1,82 @@ +"""Run a small deterministic IX-Sally cognition and governance demonstration.""" + +from __future__ import annotations + +import sys + +from ix_sally.cognition import ( + ActionSpec, + CognitiveValue, + FactEffect, + FactPattern, + FactStatus, + GoalSpec, + SallyCognitiveSystem, + WorldFact, +) +from ix_sally.digest import DigestRecord, stable_json + + +def main() -> None: + """Build a bounded plan and bridge it into IX-Sally's proposal control plane.""" + system = SallyCognitiveSystem.create() + observation = DigestRecord.from_payload({"sensor": "demo", "machine_state": "off"}) + system.observe( + WorldFact.create( + fact_id="demo-machine-off", + subject="machine", + predicate="state", + value=CognitiveValue.from_python("off"), + status=FactStatus.OBSERVED, + confidence=1.0, + evidence_digests=(observation,), + ) + ) + ready = FactPattern.create( + subject="machine", + predicate="state", + value=CognitiveValue.from_python("ready"), + ) + system.register_action( + ActionSpec.create( + action_id="prepare-demo-machine", + description="Prepare the simulated demonstration machine.", + preconditions=( + FactPattern.create( + subject="machine", + predicate="state", + value=CognitiveValue.from_python("off"), + ), + ), + effects=( + FactEffect.create( + subject="machine", + predicate="state", + value=CognitiveValue.from_python("ready"), + ), + ), + cost=1.0, + risk=0.05, + ) + ) + system.register_goal( + GoalSpec.create( + goal_id="prepare-demo-machine", + description="Reach the simulated ready state.", + desired_state=ready, + priority=1.0, + utility=1.0, + risk_limit=0.2, + evidence_digests=(observation,), + ) + ) + decision = system.deliberate( + task="Prepare the simulated demonstration machine.", + use_calibration_gate=False, + ) + bridged = system.bridge_decision(decision, cycle=0) + sys.stdout.write(f"{stable_json(bridged.to_payload())}\n") + + +if __name__ == "__main__": + main() diff --git a/package_smoke.py b/package_smoke.py index 699ae2a..5237ea9 100644 --- a/package_smoke.py +++ b/package_smoke.py @@ -88,12 +88,7 @@ def _source_version( repository_root: Path, ) -> str: """Return the canonical version declared by the source package.""" - version_path = ( - repository_root - / "src" - / "ix_sally" - / "version.py" - ) + version_path = repository_root / "src" / "ix_sally" / "version.py" tree = ast.parse( version_path.read_text(encoding="utf-8"), filename=str(version_path), @@ -104,23 +99,16 @@ def _source_version( continue if not any( - isinstance(target, ast.Name) - and target.id == "__version__" + isinstance(target, ast.Name) and target.id == "__version__" for target in statement.targets ): continue value = statement.value - if ( - isinstance(value, ast.Constant) - and isinstance(value.value, str) - and value.value - ): + if isinstance(value, ast.Constant) and isinstance(value.value, str) and value.value: return value.value - raise RuntimeError( - "IX-Sally source version is missing or is not a string literal" - ) + raise RuntimeError("IX-Sally source version is missing or is not a string literal") def _ignored_source_entries( @@ -179,16 +167,9 @@ def _build_wheel( cwd=source_root, ) - wheels = tuple( - sorted( - wheel_directory.glob("ix_sally-*.whl") - ) - ) + wheels = tuple(sorted(wheel_directory.glob("ix_sally-*.whl"))) if len(wheels) != 1: - raise RuntimeError( - "expected exactly one IX-Sally wheel, " - f"found {len(wheels)}" - ) + raise RuntimeError(f"expected exactly one IX-Sally wheel, found {len(wheels)}") return wheels[0] @@ -205,9 +186,7 @@ def _install_wheel( clear=True, ).create(environment_root) - python_executable = _venv_python( - environment_root - ) + python_executable = _venv_python(environment_root) _run( ( str(python_executable), @@ -248,9 +227,7 @@ def _verify_installed_package( environment=environment, ) if import_check.stderr: - raise RuntimeError( - import_check.stderr - ) + raise RuntimeError(import_check.stderr) module_result = _run( ( @@ -262,25 +239,15 @@ def _verify_installed_package( cwd=working_root, environment=environment, ) - payload = json.loads( - module_result.stdout - ) + payload = json.loads(module_result.stdout) if payload.get("package") != _PACKAGE_NAME: - raise RuntimeError( - "installed module reported an unexpected package name" - ) + raise RuntimeError("installed module reported an unexpected package name") if payload.get("version") != expected_version: - raise RuntimeError( - "installed module reported an unexpected package version" - ) + raise RuntimeError("installed module reported an unexpected package version") console_result = _run( ( - str( - _venv_console_script( - environment_root - ) - ), + str(_venv_console_script(environment_root)), "--baseline-digest", ), cwd=working_root, @@ -289,26 +256,16 @@ def _verify_installed_package( digest_line = console_result.stdout.strip() prefix, separator, digest_value = digest_line.partition(":") - if ( - prefix != "sha256" - or separator != ":" - or len(digest_value) != _SHA256_HEX_LENGTH - ): - raise RuntimeError( - "installed console script returned an invalid baseline digest" - ) + if prefix != "sha256" or separator != ":" or len(digest_value) != _SHA256_HEX_LENGTH: + raise RuntimeError("installed console script returned an invalid baseline digest") def main() -> int: """Build, install, and verify the current IX-Sally wheel.""" repository_root = Path(__file__).resolve().parent - expected_version = _source_version( - repository_root=repository_root - ) + expected_version = _source_version(repository_root=repository_root) - with tempfile.TemporaryDirectory( - prefix="ix-sally-package-smoke-" - ) as directory: + with tempfile.TemporaryDirectory(prefix="ix-sally-package-smoke-") as directory: temporary_root = Path(directory) source_root = temporary_root / "source" @@ -333,9 +290,7 @@ def main() -> int: expected_version=expected_version, ) - sys.stdout.write( - "Installed IX-Sally wheel smoke test passed.\n" - ) + sys.stdout.write("Installed IX-Sally wheel smoke test passed.\n") return 0 diff --git a/pyproject.toml b/pyproject.toml index fcd95aa..a7d0c5f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "ix-sally" dynamic = ["version"] -description = "Governed multi-agent autonomy habitat with doctrine, evidence gates, memory law, sandboxed action, transfer testing, and human boundary authority." +description = "Governed experimental cognitive runtime with typed IX execution, active memory, world modeling, planning, learning, and human authority boundaries." requires-python = ">=3.11" license = { text = "LicenseRef-IX-Sally-Source-Available-Evaluation-1.0" } authors = [ @@ -17,8 +17,10 @@ keywords = [ "ai-safety", "autonomy", "evidence", + "cognitive-architecture", "human-authority", - "multi-agent", + "machine-learning-research", + "planning", "source-available" ] classifiers = [ @@ -89,10 +91,17 @@ select = [ "W" ] ignore = [ - "PLR0913" + "PLR0913", + "PLR0911", + "PLR0912", + "PLR0915", + "PLR2004", ] [tool.ruff.lint.per-file-ignores] +"src/ix_sally/cli.py" = ["PLC0415"] +"src/ix_sally/cognition/system.py" = ["PLC0415"] +"src/ix_sally/runtime.py" = ["PLC0415"] "tests/**/*.py" = [ "PLR2004", "S101" diff --git a/repository_check.py b/repository_check.py index 6c39e05..0cb0d1f 100644 --- a/repository_check.py +++ b/repository_check.py @@ -20,9 +20,7 @@ } ) _PYTHON_MODULE_PATTERN: Final = re.compile(r"^[a-z][a-z0-9_]*\.py$") -_SPECIAL_PYTHON_FILES: Final[frozenset[str]] = frozenset( - {"__init__.py", "__main__.py"} -) +_SPECIAL_PYTHON_FILES: Final[frozenset[str]] = frozenset({"__init__.py", "__main__.py"}) _REQUIRED_PATHS: Final[tuple[str, ...]] = ( "LICENSE", "pyproject.toml", @@ -124,9 +122,7 @@ def _path_component_violations( RepositoryViolation( rule="path-component", path=relative, - detail=( - f"component {component!r} ends with a space or period" - ), + detail=(f"component {component!r} ends with a space or period"), ) ) @@ -162,9 +158,7 @@ def _python_filename_violations( repository_root=repository_root, path=path, ), - detail=( - "Python modules must use lowercase snake_case filenames" - ), + detail=("Python modules must use lowercase snake_case filenames"), ) ) return tuple(violations) @@ -181,11 +175,7 @@ def _package_marker_violations( violations: list[RepositoryViolation] = [] directories = {package_root} - directories.update( - path.parent - for path in package_root.rglob("*.py") - if path.is_file() - ) + directories.update(path.parent for path in package_root.rglob("*.py") if path.is_file()) for directory in sorted(directories): marker = directory / "__init__.py" @@ -233,18 +223,12 @@ def _python_source_violations( try: ast.parse(source, filename=str(path)) except SyntaxError as error: - location = ( - f"line {error.lineno}" - if error.lineno is not None - else "unknown line" - ) + location = f"line {error.lineno}" if error.lineno is not None else "unknown line" violations.append( RepositoryViolation( rule="python-syntax", path=relative, - detail=( - f"source does not parse at {location}: {error.msg}" - ), + detail=(f"source does not parse at {location}: {error.msg}"), ) ) return tuple(violations) @@ -306,13 +290,10 @@ def main() -> int: return 1 scanned_files = sum( - 1 - for path in _scanned_paths(repository_root=repository_root) - if path.is_file() + 1 for path in _scanned_paths(repository_root=repository_root) if path.is_file() ) sys.stdout.write( - "IX-Sally repository integrity passed: " - f"{scanned_files} source/test files, 0 violations.\n" + f"IX-Sally repository integrity passed: {scanned_files} source/test files, 0 violations.\n" ) return 0 diff --git a/src/ix_sally/__init__.py b/src/ix_sally/__init__.py index 57d64a0..b985412 100644 --- a/src/ix_sally/__init__.py +++ b/src/ix_sally/__init__.py @@ -24,6 +24,11 @@ AgentArtifactKind, AgentArtifactLedger, ) + from ix_sally.authority_processing import ( + AuthorityBatchProcessingResult, + AuthorityProcessingResult, + AuthorityProcessor, + ) from ix_sally.authorization import ( AuthorityDecision, AuthorityDecisionLedger, @@ -31,11 +36,6 @@ AuthorityRequest, decide_authority_request, ) - from ix_sally.authority_processing import ( - AuthorityBatchProcessingResult, - AuthorityProcessingResult, - AuthorityProcessor, - ) from ix_sally.boundaries import ( BoundaryFinding, BoundarySeverity, @@ -47,15 +47,22 @@ StopReason, ) from ix_sally.chamber_closing import ( + ChamberCloser, ChamberCloseResult, ChamberCloseStatus, - ChamberCloser, ) from ix_sally.claims import ( ClaimLedger, ClaimRecord, ClaimStatus, ) + from ix_sally.cognition.compiler import compile_ix_source + from ix_sally.cognition.evaluation import ( + CognitiveEvaluationReport, + run_core_evaluation, + ) + from ix_sally.cognition.system import SallyCognitiveSystem + from ix_sally.cognition.vm import IXVirtualMachine from ix_sally.contracts import ( AutonomyContract, AutonomyMode, @@ -100,8 +107,8 @@ ) from ix_sally.execution_dispatch import ( ExecutionDispatchBatchResult, - ExecutionDispatchResult, ExecutionDispatcher, + ExecutionDispatchResult, ) from ix_sally.execution_planning import ( ExecutionPlanner, @@ -185,9 +192,9 @@ from ix_sally.state import NinefoldRunState from ix_sally.state_audit import ( StateAuditFinding, + StateAuditor, StateAuditReport, StateAuditSeverity, - StateAuditor, ) from ix_sally.transfer import ( TransferStatus, @@ -196,7 +203,6 @@ ) __all__ = [ - "__version__", "ActionStatus", "AgentArtifact", "AgentArtifactKind", @@ -227,6 +233,7 @@ "ClaimStatus", "ClerkDocketEntry", "ClerkDocketPacket", + "CognitiveEvaluationReport", "CycleCoordinationStatus", "DigestRecord", "DocketEntryKind", @@ -267,6 +274,7 @@ "ForgeResultRecord", "ForgeResultStatus", "FoundationError", + "IXVirtualMachine", "JurisdictionDecision", "JurisdictionGate", "JurisdictionStatus", @@ -288,6 +296,7 @@ "RuntimeEvent", "RuntimeEventType", "RuntimeTranscript", + "SallyCognitiveSystem", "SallyProposalIntake", "SallyProposalIntakeResult", "SallyProposalPacket", @@ -305,14 +314,37 @@ "VerityEvidenceJudgment", "VerityEvidenceSupportReview", "VerityJudgmentPacket", + "__version__", + "compile_ix_source", "decide_authority_request", "default_agent_role_registry", + "run_core_evaluation", "session_one_baseline_digest", "session_one_baseline_payload", "session_one_contract", "session_one_runtime_kit", ] _LAZY_EXPORTS: Final[dict[str, tuple[str, str]]] = { + "CognitiveEvaluationReport": ( + "ix_sally.cognition.evaluation", + "CognitiveEvaluationReport", + ), + "IXVirtualMachine": ( + "ix_sally.cognition.vm", + "IXVirtualMachine", + ), + "SallyCognitiveSystem": ( + "ix_sally.cognition.system", + "SallyCognitiveSystem", + ), + "compile_ix_source": ( + "ix_sally.cognition.compiler", + "compile_ix_source", + ), + "run_core_evaluation": ( + "ix_sally.cognition.evaluation", + "run_core_evaluation", + ), "ActionStatus": ( "ix_sally.actions", "ActionStatus", @@ -693,7 +725,7 @@ "ix_sally.boundaries", "SentinelBoundaryReport", ), - "StateAuditFinding": ( + "StateAuditFinding": ( "ix_sally.state_audit", "StateAuditFinding", ), @@ -776,9 +808,7 @@ def __getattr__(name: str) -> object: """Load a public package export only when it is first requested.""" target = _LAZY_EXPORTS.get(name) if target is None: - raise AttributeError( - f"module {__name__!r} has no attribute {name!r}" - ) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") module_name, attribute_name = target value = getattr( @@ -791,6 +821,4 @@ def __getattr__(name: str) -> object: def __dir__() -> list[str]: """Return package globals and lazy public exports for introspection.""" - return sorted( - set(globals()) | set(__all__) - ) + return sorted(set(globals()) | set(__all__)) diff --git a/src/ix_sally/actions.py b/src/ix_sally/actions.py index 6a9906d..fceb200 100644 --- a/src/ix_sally/actions.py +++ b/src/ix_sally/actions.py @@ -2,9 +2,9 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass from enum import StrEnum -from typing import Iterable from ix_sally.agents import AgentRole from ix_sally.authorization import AuthorityDecision, AuthorityDecisionStatus, AuthorityRequest @@ -91,9 +91,11 @@ def create( if requires_tool and normalized_tool is None: raise FoundationError("bounded tool actions require a tool key") - if status in {ActionStatus.DENIED, ActionStatus.BLOCKED}: - if normalized_boundary_note is None: - raise FoundationError("denied or blocked bounded actions require a boundary note") + if ( + status in {ActionStatus.DENIED, ActionStatus.BLOCKED} + and normalized_boundary_note is None + ): + raise FoundationError("denied or blocked bounded actions require a boundary note") if status is ActionStatus.AUTHORIZED and authority_decision_digest is None: raise FoundationError("authorized bounded actions require an authority decision digest") diff --git a/src/ix_sally/agents.py b/src/ix_sally/agents.py index a4edb95..26ed61b 100644 --- a/src/ix_sally/agents.py +++ b/src/ix_sally/agents.py @@ -2,9 +2,9 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass from enum import StrEnum -from typing import Iterable from ix_sally.digest import DigestRecord, JsonArray, JsonObject from ix_sally.foundation import CanonicalKey, FoundationError, require_text @@ -138,7 +138,10 @@ def default_agent_role_registry() -> AgentRoleRegistry: AgentRoleDefinition.create( role=AgentRole.BUTCH, title="Adversary and falsifier", - duty="Attacks assumptions, contradictions, weak plans, unsupported claims, and drift.", + duty=( + "Attacks assumptions, contradictions, weak plans, " + "unsupported claims, and drift." + ), prohibited_authorities=( "verified-memory-write", "execution-approval", @@ -148,7 +151,10 @@ def default_agent_role_registry() -> AgentRoleRegistry: AgentRoleDefinition.create( role=AgentRole.VERITY, title="Evidence judge", - duty="Judges whether claims are supported, unsupported, contradicted, blocked, or pending.", + duty=( + "Judges whether claims are supported, unsupported, " + "contradicted, blocked, or pending." + ), prohibited_authorities=( "tool-execution", "memory-storage", @@ -168,7 +174,9 @@ def default_agent_role_registry() -> AgentRoleRegistry: AgentRoleDefinition.create( role=AgentRole.FORGE, title="Executor and sandbox tester", - duty="Runs allowed tool actions, command checks, and executable tests inside scope.", + duty=( + "Runs allowed tool actions, command checks, and executable tests inside scope." + ), prohibited_authorities=( "final-evidence-verdict", "verified-memory-write", @@ -178,7 +186,10 @@ def default_agent_role_registry() -> AgentRoleRegistry: AgentRoleDefinition.create( role=AgentRole.MNEMOSYNE, title="Memory law and learning keeper", - duty="Classifies memory candidates as pending, verified, stale, contradicted, or quarantined.", + duty=( + "Classifies memory candidates as pending, verified, stale, " + "contradicted, or quarantined." + ), prohibited_authorities=( "tool-execution", "proposal-authorship", @@ -188,7 +199,10 @@ def default_agent_role_registry() -> AgentRoleRegistry: AgentRoleDefinition.create( role=AgentRole.SENTINEL, title="Safety and boundary guard", - duty="Detects manipulation pressure, unsafe escalation, unauthorized scope, and drift.", + duty=( + "Detects manipulation pressure, unsafe escalation, " + "unauthorized scope, and drift." + ), prohibited_authorities=( "proposal-authorship", "verified-memory-write", @@ -198,7 +212,10 @@ def default_agent_role_registry() -> AgentRoleRegistry: AgentRoleDefinition.create( role=AgentRole.TRANSFER, title="Generalization tester", - duty="Tests whether learned patterns transfer beyond the originating task or context.", + duty=( + "Tests whether learned patterns transfer beyond the " + "originating task or context." + ), prohibited_authorities=( "tool-execution", "verified-memory-write", diff --git a/src/ix_sally/artifacts.py b/src/ix_sally/artifacts.py index e27755c..8dec1eb 100644 --- a/src/ix_sally/artifacts.py +++ b/src/ix_sally/artifacts.py @@ -2,9 +2,9 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass, field from enum import StrEnum -from typing import Iterable from ix_sally.agents import AgentRole from ix_sally.digest import DigestRecord, JsonArray, JsonObject diff --git a/src/ix_sally/authority_processing.py b/src/ix_sally/authority_processing.py index 82a4157..f1e0fec 100644 --- a/src/ix_sally/authority_processing.py +++ b/src/ix_sally/authority_processing.py @@ -55,9 +55,7 @@ def authorized_count(self) -> int: def human_review_count(self) -> int: """Return how many processed actions require human review.""" return sum( - 1 - for result in self.processed - if result.authority_decision.requires_human_review() + 1 for result in self.processed if result.authority_decision.requires_human_review() ) def blocked_count(self) -> int: diff --git a/src/ix_sally/authorization.py b/src/ix_sally/authorization.py index 5cd24ec..6bbd0c4 100644 --- a/src/ix_sally/authorization.py +++ b/src/ix_sally/authorization.py @@ -2,9 +2,9 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass from enum import StrEnum -from typing import Iterable from ix_sally.agents import AgentRole from ix_sally.contracts import AutonomyContract @@ -155,9 +155,11 @@ def create( if status is AuthorityDecisionStatus.DENIED and normalized_contract_note is None: raise FoundationError("denied authority decisions require a contract note") - if status is AuthorityDecisionStatus.HUMAN_REVIEW_REQUIRED: - if normalized_human_note is None: - raise FoundationError("human-review authority decisions require a human note") + if ( + status is AuthorityDecisionStatus.HUMAN_REVIEW_REQUIRED + and normalized_human_note is None + ): + raise FoundationError("human-review authority decisions require a human note") return cls( decision_id=decision_id @@ -228,7 +230,9 @@ def create(cls, decisions: Iterable[AuthorityDecision]) -> AuthorityDecisionLedg for decision in normalized: if decision.decision_id.value in seen: - raise FoundationError(f"duplicate authority decision id: {decision.decision_id.value}") + raise FoundationError( + f"duplicate authority decision id: {decision.decision_id.value}" + ) seen.add(decision.decision_id.value) return cls(decisions=normalized) @@ -243,9 +247,7 @@ def denied_decisions(self) -> tuple[AuthorityDecision, ...]: def human_review_decisions(self) -> tuple[AuthorityDecision, ...]: """Return decisions requiring human review.""" - return tuple( - decision for decision in self.decisions if decision.requires_human_review() - ) + return tuple(decision for decision in self.decisions if decision.requires_human_review()) def to_payload(self) -> JsonObject: """Return a stable JSON-compatible decision ledger representation.""" @@ -314,7 +316,9 @@ def decide_authority_request( cycle=request.cycle, request_digest=request.digest(), status=AuthorityDecisionStatus.HUMAN_REVIEW_REQUIRED, - rationale="Requested action is inside role jurisdiction but requires human boundary review.", + rationale=( + "Requested action is inside role jurisdiction but requires human boundary review." + ), jurisdiction_decision=jurisdiction, human_review_note="human boundary review is required before action execution", ) diff --git a/src/ix_sally/boundaries.py b/src/ix_sally/boundaries.py index 9ef38bc..44aef98 100644 --- a/src/ix_sally/boundaries.py +++ b/src/ix_sally/boundaries.py @@ -2,9 +2,9 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass from enum import StrEnum -from typing import Iterable from ix_sally.agents import AgentRole from ix_sally.artifacts import AgentArtifact, AgentArtifactKind @@ -60,11 +60,11 @@ def create( field_name="required_human_action", ) - if severity in {BoundarySeverity.BLOCKING, BoundarySeverity.TERMINATION}: - if normalized_action is None: - raise FoundationError( - "blocking or termination boundary findings require human action" - ) + if ( + severity in {BoundarySeverity.BLOCKING, BoundarySeverity.TERMINATION} + and normalized_action is None + ): + raise FoundationError("blocking or termination boundary findings require human action") return cls( finding_id=finding_id diff --git a/src/ix_sally/chamber_closing.py b/src/ix_sally/chamber_closing.py index 3ed4048..cb37cbe 100644 --- a/src/ix_sally/chamber_closing.py +++ b/src/ix_sally/chamber_closing.py @@ -9,7 +9,7 @@ from ix_sally.foundation import require_text from ix_sally.recording import StateRecorder from ix_sally.state import NinefoldRunState -from ix_sally.state_audit import StateAuditReport, StateAuditor +from ix_sally.state_audit import StateAuditor, StateAuditReport class ChamberCloseStatus(StrEnum): diff --git a/src/ix_sally/claims.py b/src/ix_sally/claims.py index 9ed4d3c..b51c637 100644 --- a/src/ix_sally/claims.py +++ b/src/ix_sally/claims.py @@ -2,9 +2,9 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass, field from enum import StrEnum -from typing import Iterable from ix_sally.agents import AgentRole from ix_sally.digest import DigestRecord, JsonArray, JsonObject diff --git a/src/ix_sally/cli.py b/src/ix_sally/cli.py index b238e56..00f8fe8 100644 --- a/src/ix_sally/cli.py +++ b/src/ix_sally/cli.py @@ -5,6 +5,7 @@ import argparse import sys from collections.abc import Sequence +from pathlib import Path from ix_sally.digest import stable_json from ix_sally.session_baseline import ( @@ -18,7 +19,7 @@ def build_parser() -> argparse.ArgumentParser: """Build the IX-Sally command-line parser.""" parser = argparse.ArgumentParser( prog="ix-sally", - description="IX-Sally governed ninefold autonomy runtime.", + description="IX-Sally governed cognitive and ninefold autonomy runtime.", ) parser.add_argument( "--runtime-baseline", @@ -30,6 +31,22 @@ def build_parser() -> argparse.ArgumentParser: action="store_true", help="Print the deterministic session-one baseline SHA-256 digest.", ) + parser.add_argument( + "--cognitive-evaluation", + action="store_true", + help="Run the built-in cognitive capability suite and print its report as JSON.", + ) + parser.add_argument( + "--execute-ix", + type=Path, + metavar="FILE", + help="Compile and execute one UTF-8 IX source file in the bounded local VM.", + ) + parser.add_argument( + "--empty-cognitive-snapshot", + action="store_true", + help="Print a canonical snapshot for a clean cognitive runtime.", + ) return parser @@ -39,21 +56,42 @@ def main(argv: Sequence[str] | None = None) -> int: args = parser.parse_args(argv) if args.runtime_baseline: - sys.stdout.write( - f"{stable_json(session_one_baseline_payload())}\n" - ) + sys.stdout.write(f"{stable_json(session_one_baseline_payload())}\n") return 0 if args.baseline_digest: digest = session_one_baseline_digest() - sys.stdout.write( - f"{digest.algorithm}:{digest.value}\n" + sys.stdout.write(f"{digest.algorithm}:{digest.value}\n") + return 0 + + if args.cognitive_evaluation: + from ix_sally.cognition.evaluation import run_core_evaluation + + report = run_core_evaluation() + sys.stdout.write(f"{stable_json(report.to_payload())}\n") + return 0 if report.passed() == len(report.results) else 1 + + if args.execute_ix is not None: + from ix_sally.cognition.system import SallyCognitiveSystem + + try: + source = args.execute_ix.read_text(encoding="utf-8") + except OSError as exc: + parser.error(f"could not read IX source file: {exc}") + result = SallyCognitiveSystem.create().execute_ix( + source, + filename=str(args.execute_ix), ) + sys.stdout.write(f"{stable_json(result.to_payload())}\n") + return 0 if result.status.value == "halted" else 1 + + if args.empty_cognitive_snapshot: + from ix_sally.cognition.system import SallyCognitiveSystem + + sys.stdout.write(f"{SallyCognitiveSystem.create().snapshot().to_json()}\n") return 0 - sys.stdout.write( - f"IX-Sally {__version__}\n" - ) + sys.stdout.write(f"IX-Sally {__version__}\n") return 0 diff --git a/src/ix_sally/cognition/__init__.py b/src/ix_sally/cognition/__init__.py new file mode 100644 index 0000000..789bc02 --- /dev/null +++ b/src/ix_sally/cognition/__init__.py @@ -0,0 +1,217 @@ +"""Integrated cognitive architecture for the IX-Sally research runtime.""" + +from ix_sally.cognition.active_memory import ( + ActiveMemoryEntry, + ActiveMemoryStatus, + ActiveMemoryStore, + MemoryLayer, + MemoryRetrieval, +) +from ix_sally.cognition.adaptation import ( + AdaptationController, + RegressionFinding, + RegressionOutcome, + RegressionReport, +) +from ix_sally.cognition.bytecode import BytecodeProgram, Instruction, OpCode +from ix_sally.cognition.compiler import IXCompiler, compile_ix_source +from ix_sally.cognition.curriculum import ( + Curriculum, + CurriculumLedger, + CurriculumSplit, + CurriculumTask, + CurriculumTrial, + TrialStatus, +) +from ix_sally.cognition.episodes import ( + CognitiveEpisode, + EpisodeLedger, + EpisodeStep, + EpisodeStepKind, + EpisodeStepStatus, +) +from ix_sally.cognition.evaluation import ( + BenchmarkResult, + CognitiveEvaluationReport, + EvaluationCategory, + run_core_evaluation, +) +from ix_sally.cognition.executive import ( + ExecutiveController, + ExecutiveDecision, + ExecutiveDecisionStatus, +) +from ix_sally.cognition.goals import GoalGraph, GoalSpec, GoalStatus +from ix_sally.cognition.governance_bridge import ( + CognitiveProposalBridge, + CognitiveProposalBridgeReceipt, + CognitiveProposalBridgeResult, +) +from ix_sally.cognition.learning import ( + LearningLedger, + LearningOutcome, + OutcomeStatus, + SkillProfile, + TransferEvaluation, +) +from ix_sally.cognition.metacognition import ( + CapabilityMeasure, + ImprovementProposal, + ImprovementStatus, + SelfModel, +) +from ix_sally.cognition.ninefold import ( + NinefoldCognitiveCycle, + NinefoldCoordinator, + RoleFinding, +) +from ix_sally.cognition.persistence import CognitiveSnapshot +from ix_sally.cognition.planning import ( + ActionSpec, + DeterministicPlanner, + ExecutionPermission, + FactEffect, + Plan, + PlanExecutionReceipt, + PlanSimulator, + PlanStatus, +) +from ix_sally.cognition.primitives import ( + PrimitiveExecution, + PrimitiveExecutor, + PrimitiveKind, + PrimitiveOperation, + PrimitiveRegistry, + PrimitiveSpec, + PrimitiveStatus, + default_primitive_registry, +) +from ix_sally.cognition.storage import ( + SnapshotLoadResult, + SnapshotRepository, + SnapshotSaveReceipt, + SnapshotSource, +) +from ix_sally.cognition.system import SallyCognitiveSystem +from ix_sally.cognition.uncertainty import ( + CalibrationBin, + CalibrationObservation, + CalibrationReport, + UncertaintyLedger, +) +from ix_sally.cognition.values import ( + CognitiveScalar, + CognitiveValue, + CognitiveValueType, + value_from_payload, +) +from ix_sally.cognition.vm import IXVirtualMachine, VMResult, VMStatus, VMTraceEntry +from ix_sally.cognition.workspace import ( + CognitiveWorkspace, + WorkspaceItem, + WorkspaceItemKind, + WorkspaceItemStatus, +) +from ix_sally.cognition.world_model import ( + CausalRule, + FactPattern, + FactStatus, + WorldFact, + WorldModel, +) + +__all__ = [ + "ActionSpec", + "ActiveMemoryEntry", + "ActiveMemoryStatus", + "ActiveMemoryStore", + "AdaptationController", + "BenchmarkResult", + "BytecodeProgram", + "CalibrationBin", + "CalibrationObservation", + "CalibrationReport", + "CapabilityMeasure", + "CausalRule", + "CognitiveEpisode", + "CognitiveEvaluationReport", + "CognitiveProposalBridge", + "CognitiveProposalBridgeReceipt", + "CognitiveProposalBridgeResult", + "CognitiveScalar", + "CognitiveSnapshot", + "CognitiveValue", + "CognitiveValueType", + "CognitiveWorkspace", + "Curriculum", + "CurriculumLedger", + "CurriculumSplit", + "CurriculumTask", + "CurriculumTrial", + "DeterministicPlanner", + "EpisodeLedger", + "EpisodeStep", + "EpisodeStepKind", + "EpisodeStepStatus", + "EvaluationCategory", + "ExecutionPermission", + "ExecutiveController", + "ExecutiveDecision", + "ExecutiveDecisionStatus", + "FactEffect", + "FactPattern", + "FactStatus", + "GoalGraph", + "GoalSpec", + "GoalStatus", + "IXCompiler", + "IXVirtualMachine", + "ImprovementProposal", + "ImprovementStatus", + "Instruction", + "LearningLedger", + "LearningOutcome", + "MemoryLayer", + "MemoryRetrieval", + "NinefoldCognitiveCycle", + "NinefoldCoordinator", + "OpCode", + "OutcomeStatus", + "Plan", + "PlanExecutionReceipt", + "PlanSimulator", + "PlanStatus", + "PrimitiveExecution", + "PrimitiveExecutor", + "PrimitiveKind", + "PrimitiveOperation", + "PrimitiveRegistry", + "PrimitiveSpec", + "PrimitiveStatus", + "RegressionFinding", + "RegressionOutcome", + "RegressionReport", + "RoleFinding", + "SallyCognitiveSystem", + "SelfModel", + "SkillProfile", + "SnapshotLoadResult", + "SnapshotRepository", + "SnapshotSaveReceipt", + "SnapshotSource", + "TransferEvaluation", + "TrialStatus", + "UncertaintyLedger", + "VMResult", + "VMStatus", + "VMTraceEntry", + "WorkspaceItem", + "WorkspaceItemKind", + "WorkspaceItemStatus", + "WorldFact", + "WorldModel", + "compile_ix_source", + "default_primitive_registry", + "run_core_evaluation", + "value_from_payload", +] diff --git a/src/ix_sally/cognition/active_memory.py b/src/ix_sally/cognition/active_memory.py new file mode 100644 index 0000000..fca56c0 --- /dev/null +++ b/src/ix_sally/cognition/active_memory.py @@ -0,0 +1,304 @@ +"""Working, episodic, semantic, and procedural memory with truth boundaries.""" + +from __future__ import annotations + +import re +from collections.abc import Iterable +from dataclasses import dataclass +from enum import StrEnum + +from ix_sally.digest import DigestRecord, JsonArray, JsonObject, JsonValue +from ix_sally.foundation import CanonicalKey, FoundationError, require_text + +_TOKEN_PATTERN = re.compile(r"[a-z0-9]+") + + +class MemoryLayer(StrEnum): + """Distinct stores used by the active memory system.""" + + WORKING = "working" + EPISODIC = "episodic" + SEMANTIC = "semantic" + PROCEDURAL = "procedural" + + +class ActiveMemoryStatus(StrEnum): + """Truth and lifecycle status of one active memory entry.""" + + CANDIDATE = "candidate" + VERIFIED = "verified" + STALE = "stale" + CONTRADICTED = "contradicted" + QUARANTINED = "quarantined" + RETIRED = "retired" + + +@dataclass(frozen=True, slots=True) +class ActiveMemoryEntry: + """One versioned memory entry with content, confidence, and provenance.""" + + memory_id: CanonicalKey + layer: MemoryLayer + content: str + confidence: float + status: ActiveMemoryStatus + sequence: int + evidence_digests: tuple[DigestRecord, ...] = () + source_ids: tuple[CanonicalKey, ...] = () + tags: tuple[CanonicalKey, ...] = () + reason: str | None = None + + @classmethod + def create( + cls, + *, + memory_id: str, + layer: MemoryLayer, + content: str, + confidence: float, + status: ActiveMemoryStatus, + sequence: int, + evidence_digests: Iterable[DigestRecord] = (), + source_ids: Iterable[str] = (), + tags: Iterable[str] = (), + reason: str | None = None, + ) -> ActiveMemoryEntry: + """Create a memory entry without silently promoting content to truth.""" + if not 0.0 <= confidence <= 1.0: + raise FoundationError("active memory confidence must be between 0 and 1") + if sequence < 0: + raise FoundationError("active memory sequence must not be negative") + evidence = tuple(evidence_digests) + for digest in evidence: + digest.require_algorithm("sha256") + if status is ActiveMemoryStatus.VERIFIED and not evidence: + raise FoundationError("verified active memory requires evidence") + if ( + status + in { + ActiveMemoryStatus.CONTRADICTED, + ActiveMemoryStatus.QUARANTINED, + ActiveMemoryStatus.RETIRED, + } + and not reason + ): + raise FoundationError("inactive active memory requires a reason") + return cls( + memory_id=CanonicalKey.from_text(memory_id, field_name="memory_id"), + layer=layer, + content=require_text(content, field_name="content"), + confidence=confidence, + status=status, + sequence=sequence, + evidence_digests=evidence, + source_ids=tuple( + sorted( + { + CanonicalKey.from_text(source_id, field_name="source_id") + for source_id in source_ids + }, + key=lambda item: item.value, + ) + ), + tags=tuple( + sorted( + {CanonicalKey.from_text(tag, field_name="tag") for tag in tags}, + key=lambda item: item.value, + ) + ), + reason=require_text(reason, field_name="reason") if reason else None, + ) + + def is_retrievable_truth(self) -> bool: + """Return whether this entry may support a factual answer.""" + return self.status is ActiveMemoryStatus.VERIFIED + + def tokens(self) -> frozenset[str]: + """Return normalized lexical tokens used by deterministic retrieval.""" + return frozenset(_TOKEN_PATTERN.findall(self.content.lower())) + + def to_payload(self) -> JsonObject: + """Return a canonical active-memory payload.""" + evidence: JsonArray = [ + {"algorithm": digest.algorithm, "value": digest.value} + for digest in self.evidence_digests + ] + sources: JsonArray = [source.value for source in self.source_ids] + tags: JsonArray = [tag.value for tag in self.tags] + return { + "memory_id": self.memory_id.value, + "layer": self.layer.value, + "content": self.content, + "confidence": self.confidence, + "status": self.status.value, + "sequence": self.sequence, + "evidence_digests": evidence, + "source_ids": sources, + "tags": tags, + "reason": self.reason, + } + + def digest(self) -> DigestRecord: + """Return a deterministic entry identity.""" + return DigestRecord.from_payload(self.to_payload()) + + +@dataclass(frozen=True, slots=True) +class MemoryRetrieval: + """One scored memory retrieval result.""" + + entry: ActiveMemoryEntry + lexical_score: float + confidence_score: float + recency_score: float + total_score: float + + def to_payload(self) -> JsonObject: + """Return a deterministic retrieval receipt.""" + return { + "entry_digest": { + "algorithm": self.entry.digest().algorithm, + "value": self.entry.digest().value, + }, + "lexical_score": self.lexical_score, + "confidence_score": self.confidence_score, + "recency_score": self.recency_score, + "total_score": self.total_score, + } + + +@dataclass(frozen=True, slots=True) +class ActiveMemoryStore: + """Immutable multi-layer memory store with bounded deterministic retrieval.""" + + entries: tuple[ActiveMemoryEntry, ...] = () + + def __post_init__(self) -> None: + """Reject duplicate identifiers and non-monotonic versions per identity.""" + identifiers = [entry.memory_id.value for entry in self.entries] + if len(identifiers) != len(set(identifiers)): + raise FoundationError("active memory contains duplicate identifiers") + + def append(self, entry: ActiveMemoryEntry) -> ActiveMemoryStore: + """Return a store with one unique entry appended.""" + if any(existing.memory_id == entry.memory_id for existing in self.entries): + raise FoundationError(f"active memory already exists: {entry.memory_id.value}") + return ActiveMemoryStore((*self.entries, entry)) + + def require(self, memory_id: str) -> ActiveMemoryEntry: + """Return one memory entry by canonical identifier.""" + requested = CanonicalKey.from_text(memory_id, field_name="memory_id") + for entry in self.entries: + if entry.memory_id == requested: + return entry + raise FoundationError(f"unknown active memory: {requested.value}") + + def replace(self, entry: ActiveMemoryEntry) -> ActiveMemoryStore: + """Replace an entry while requiring a strictly later sequence.""" + current = self.require(entry.memory_id.value) + if entry.sequence <= current.sequence: + raise FoundationError("active memory replacement sequence must increase") + return ActiveMemoryStore( + tuple( + entry if existing.memory_id == entry.memory_id else existing + for existing in self.entries + ) + ) + + def retrieve( + self, + query: str, + *, + layers: Iterable[MemoryLayer] = tuple(MemoryLayer), + limit: int = 5, + truth_only: bool = False, + ) -> tuple[MemoryRetrieval, ...]: + """Retrieve entries using transparent lexical, confidence, and recency scores.""" + normalized_query = require_text(query, field_name="query") + if limit <= 0: + raise FoundationError("memory retrieval limit must be positive") + layer_set = frozenset(layers) + query_tokens = frozenset(_TOKEN_PATTERN.findall(normalized_query.lower())) + max_sequence = max((entry.sequence for entry in self.entries), default=0) + results: list[MemoryRetrieval] = [] + for entry in self.entries: + if entry.layer not in layer_set: + continue + if truth_only and not entry.is_retrievable_truth(): + continue + overlap = len(query_tokens & entry.tokens()) + union = len(query_tokens | entry.tokens()) + lexical = overlap / union if union else 0.0 + recency = entry.sequence / max_sequence if max_sequence else 0.0 + total = round(0.55 * lexical + 0.3 * entry.confidence + 0.15 * recency, 12) + results.append( + MemoryRetrieval( + entry=entry, + lexical_score=round(lexical, 12), + confidence_score=entry.confidence, + recency_score=round(recency, 12), + total_score=total, + ) + ) + return tuple( + sorted( + results, + key=lambda result: ( + -result.total_score, + result.entry.memory_id.value, + ), + )[:limit] + ) + + def consolidate( + self, + *, + memory_id: str, + source_ids: Iterable[str], + content: str, + evidence_digests: Iterable[DigestRecord], + confidence: float, + ) -> ActiveMemoryStore: + """Create semantic memory from verified source entries only.""" + normalized_sources = tuple(source_ids) + if not normalized_sources: + raise FoundationError("memory consolidation requires source entries") + sources = tuple(self.require(source_id) for source_id in normalized_sources) + if any(not source.is_retrievable_truth() for source in sources): + raise FoundationError("memory consolidation requires verified source entries") + sequence = max((entry.sequence for entry in self.entries), default=-1) + 1 + consolidated = ActiveMemoryEntry.create( + memory_id=memory_id, + layer=MemoryLayer.SEMANTIC, + content=content, + confidence=confidence, + status=ActiveMemoryStatus.VERIFIED, + sequence=sequence, + evidence_digests=evidence_digests, + source_ids=normalized_sources, + tags=("consolidated",), + ) + return self.append(consolidated) + + def to_payload(self) -> JsonObject: + """Return a canonical memory-store payload.""" + entries: JsonArray = [ + entry.to_payload() + for entry in sorted( + self.entries, + key=lambda candidate: candidate.memory_id.value, + ) + ] + layer_counts: dict[str, JsonValue] = { + layer.value: sum(1 for entry in self.entries if entry.layer is layer) + for layer in MemoryLayer + } + return { + "entry_count": len(self.entries), + "layer_counts": layer_counts, + "entries": entries, + } + + def digest(self) -> DigestRecord: + """Return a deterministic store identity.""" + return DigestRecord.from_payload(self.to_payload()) diff --git a/src/ix_sally/cognition/adaptation.py b/src/ix_sally/cognition/adaptation.py new file mode 100644 index 0000000..5c43a00 --- /dev/null +++ b/src/ix_sally/cognition/adaptation.py @@ -0,0 +1,185 @@ +"""Regression-aware adaptation proposals that cannot approve or apply themselves.""" + +from __future__ import annotations + +from collections.abc import Iterable +from dataclasses import dataclass +from enum import StrEnum + +from ix_sally.cognition.metacognition import ImprovementProposal, SelfModel +from ix_sally.digest import DigestRecord, JsonArray, JsonObject +from ix_sally.foundation import CanonicalKey, FoundationError, require_text + + +class RegressionOutcome(StrEnum): + """Result of comparing measured capabilities before and after a candidate change.""" + + IMPROVED = "improved" + NEUTRAL = "neutral" + REGRESSED = "regressed" + INCOMPLETE = "incomplete" + + +@dataclass(frozen=True, slots=True) +class RegressionFinding: + """One exact capability comparison for a candidate improvement.""" + + capability_id: CanonicalKey + before_score: float | None + after_score: float | None + delta: float | None + outcome: RegressionOutcome + + def to_payload(self) -> JsonObject: + """Return a canonical regression finding.""" + return { + "capability_id": self.capability_id.value, + "before_score": self.before_score, + "after_score": self.after_score, + "delta": self.delta, + "outcome": self.outcome.value, + } + + +@dataclass(frozen=True, slots=True) +class RegressionReport: + """Complete comparison between pre-change and post-change self models.""" + + proposal_digest: DigestRecord + findings: tuple[RegressionFinding, ...] + permitted_regression: float + + def __post_init__(self) -> None: + """Require a valid comparison threshold and proposal digest.""" + self.proposal_digest.require_algorithm("sha256") + if not 0.0 <= self.permitted_regression <= 1.0: + raise FoundationError("permitted regression must be between 0 and 1") + + def has_regression(self) -> bool: + """Return whether any measured capability exceeds the allowed score drop.""" + return any( + finding.delta is not None and finding.delta < -self.permitted_regression + for finding in self.findings + ) + + def complete(self) -> bool: + """Return whether every capability exists in both compared models.""" + return all(finding.outcome is not RegressionOutcome.INCOMPLETE for finding in self.findings) + + def may_request_validation(self) -> bool: + """Return whether evidence permits human review of a validation request.""" + return self.complete() and not self.has_regression() + + def to_payload(self) -> JsonObject: + """Return a canonical regression report.""" + findings: JsonArray = [finding.to_payload() for finding in self.findings] + return { + "proposal_digest": { + "algorithm": self.proposal_digest.algorithm, + "value": self.proposal_digest.value, + }, + "findings": findings, + "permitted_regression": self.permitted_regression, + "has_regression": self.has_regression(), + "complete": self.complete(), + "may_request_validation": self.may_request_validation(), + } + + def digest(self) -> DigestRecord: + """Return a deterministic report identity.""" + return DigestRecord.from_payload(self.to_payload()) + + +@dataclass(frozen=True, slots=True) +class AdaptationController: + """Generate bounded improvement proposals and compare measured outcomes.""" + + permitted_regression: float = 0.0 + + def __post_init__(self) -> None: + """Require a valid regression tolerance.""" + if not 0.0 <= self.permitted_regression <= 1.0: + raise FoundationError("permitted regression must be between 0 and 1") + + def propose_for_weakest( + self, + *, + self_model: SelfModel, + description: str, + expected_benefit: float, + regression_risk: float, + supporting_evidence: Iterable[DigestRecord], + ) -> ImprovementProposal: + """Propose work on the weakest measured capability without self-approval.""" + weakest = self_model.weakest() + if weakest is None: + raise FoundationError("cannot propose adaptation without capability measures") + evidence = tuple(supporting_evidence) + if not evidence: + raise FoundationError("adaptation proposal requires supporting evidence") + seed = DigestRecord.from_payload( + { + "self_model": self_model.digest().value, + "target": weakest.capability_id.value, + "description": require_text(description, field_name="description"), + "evidence": [item.value for item in evidence], + } + ) + return ImprovementProposal.create( + proposal_id=f"adaptation-{weakest.capability_id.value}-{seed.value[:16]}", + target_capability=weakest.capability_id.value, + description=description, + expected_benefit=expected_benefit, + regression_risk=regression_risk, + evidence_digests=evidence, + ) + + def compare( + self, + *, + proposal: ImprovementProposal, + before: SelfModel, + after: SelfModel, + ) -> RegressionReport: + """Compare every capability without hiding missing or regressed measures.""" + before_by_id = {item.capability_id: item for item in before.measures} + after_by_id = {item.capability_id: item for item in after.measures} + identifiers = tuple( + sorted(set(before_by_id) | set(after_by_id), key=lambda item: item.value) + ) + findings = [] + for capability_id in identifiers: + before_measure = before_by_id.get(capability_id) + after_measure = after_by_id.get(capability_id) + if before_measure is None or after_measure is None: + findings.append( + RegressionFinding( + capability_id=capability_id, + before_score=(before_measure.score if before_measure is not None else None), + after_score=(after_measure.score if after_measure is not None else None), + delta=None, + outcome=RegressionOutcome.INCOMPLETE, + ) + ) + continue + delta = round(after_measure.score - before_measure.score, 12) + if delta > 0: + outcome = RegressionOutcome.IMPROVED + elif delta < -self.permitted_regression: + outcome = RegressionOutcome.REGRESSED + else: + outcome = RegressionOutcome.NEUTRAL + findings.append( + RegressionFinding( + capability_id=capability_id, + before_score=before_measure.score, + after_score=after_measure.score, + delta=delta, + outcome=outcome, + ) + ) + return RegressionReport( + proposal_digest=proposal.digest(), + findings=tuple(findings), + permitted_regression=self.permitted_regression, + ) diff --git a/src/ix_sally/cognition/bytecode.py b/src/ix_sally/cognition/bytecode.py new file mode 100644 index 0000000..2af9e0f --- /dev/null +++ b/src/ix_sally/cognition/bytecode.py @@ -0,0 +1,140 @@ +"""Deterministic bytecode records for the embedded IX cognitive VM.""" + +from __future__ import annotations + +from collections.abc import Iterable +from dataclasses import dataclass +from enum import StrEnum + +from ix_sally.cognition.values import CognitiveValue +from ix_sally.digest import DigestRecord, JsonArray, JsonObject, JsonValue +from ix_sally.foundation import CanonicalKey, FoundationError, require_optional_text + + +class OpCode(StrEnum): + """Instructions supported by the dependency-free IX virtual machine.""" + + PUSH = "push" + LOAD_LOCAL = "load_local" + STORE_LOCAL = "store_local" + LOAD_MEMORY = "load_memory" + STORE_MEMORY = "store_memory" + UNARY_POSITIVE = "unary_positive" + UNARY_NEGATE = "unary_negate" + UNARY_NOT = "unary_not" + BINARY_OR = "binary_or" + BINARY_AND = "binary_and" + BINARY_EQUAL = "binary_equal" + BINARY_NOT_EQUAL = "binary_not_equal" + BINARY_GREATER = "binary_greater" + BINARY_GREATER_EQUAL = "binary_greater_equal" + BINARY_LESS = "binary_less" + BINARY_LESS_EQUAL = "binary_less_equal" + BINARY_ADD = "binary_add" + BINARY_SUBTRACT = "binary_subtract" + BINARY_MULTIPLY = "binary_multiply" + BINARY_DIVIDE = "binary_divide" + EMIT_OUTPUT = "emit_output" + EMIT_REPLY = "emit_reply" + ASSERT = "assert" + TRACE = "trace" + HALT = "halt" + + +_NAME_OPERATIONS = { + OpCode.LOAD_LOCAL, + OpCode.STORE_LOCAL, + OpCode.LOAD_MEMORY, + OpCode.STORE_MEMORY, +} + + +@dataclass(frozen=True, slots=True) +class Instruction: + """One immutable VM instruction with validated operands.""" + + opcode: OpCode + value: CognitiveValue | None = None + name: str | None = None + + def __post_init__(self) -> None: + """Require exactly the operands expected by the opcode.""" + normalized_name = require_optional_text(self.name, field_name="instruction name") + object.__setattr__(self, "name", normalized_name) + if self.opcode is OpCode.PUSH: + if self.value is None or self.name is not None: + raise FoundationError("push requires a value and no name") + return + if self.opcode in _NAME_OPERATIONS: + if self.name is None or self.value is not None: + raise FoundationError(f"{self.opcode.value} requires a name and no literal value") + return + if self.value is not None or self.name is not None: + raise FoundationError(f"{self.opcode.value} does not accept instruction operands") + + def to_payload(self) -> JsonObject: + """Return a canonical instruction payload.""" + value_payload: JsonValue = None + if self.value is not None: + value_payload = self.value.to_payload() + return { + "opcode": self.opcode.value, + "name": self.name, + "value": value_payload, + } + + +@dataclass(frozen=True, slots=True) +class BytecodeProgram: + """A complete, digest-bound sequence of VM instructions.""" + + program_id: CanonicalKey + instructions: tuple[Instruction, ...] + source_digest: DigestRecord + + @classmethod + def create( + cls, + *, + instructions: Iterable[Instruction], + source_digest: DigestRecord, + program_id: CanonicalKey | None = None, + ) -> BytecodeProgram: + """Create bytecode and require a single final halt instruction.""" + normalized = tuple(instructions) + source_digest.require_algorithm("sha256") + if not normalized: + raise FoundationError("bytecode program must contain instructions") + halt_positions = tuple( + index + for index, instruction in enumerate(normalized) + if instruction.opcode is OpCode.HALT + ) + if halt_positions != (len(normalized) - 1,): + raise FoundationError("bytecode program requires exactly one final halt") + return cls( + program_id=program_id + or CanonicalKey.from_text( + f"ix-program-{source_digest.value[:20]}", + field_name="program_id", + ), + instructions=normalized, + source_digest=source_digest, + ) + + def to_payload(self) -> JsonObject: + """Return a canonical bytecode representation.""" + instructions: JsonArray = [instruction.to_payload() for instruction in self.instructions] + return { + "program_id": self.program_id.value, + "source_digest": { + "algorithm": self.source_digest.algorithm, + "value": self.source_digest.value, + }, + "instruction_count": len(self.instructions), + "instructions": instructions, + } + + def digest(self) -> DigestRecord: + """Return a deterministic identity for this bytecode program.""" + return DigestRecord.from_payload(self.to_payload()) diff --git a/src/ix_sally/cognition/compiler.py b/src/ix_sally/cognition/compiler.py new file mode 100644 index 0000000..679304a --- /dev/null +++ b/src/ix_sally/cognition/compiler.py @@ -0,0 +1,163 @@ +"""Compiler from accepted IX syntax trees into deterministic VM bytecode.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from ix_sally.cognition.bytecode import BytecodeProgram, Instruction, OpCode +from ix_sally.cognition.values import CognitiveValue +from ix_sally.digest import DigestRecord +from ix_sally.foundation import FoundationError +from ix_sally.language.ast import ( + BinaryExpression, + BinaryOperator, + Expression, + GroupExpression, + LiteralExpression, + NameExpression, + UnaryExpression, + UnaryOperator, +) +from ix_sally.language.frontend import IXFrontendAnalysis, require_accepted_ix_source +from ix_sally.language.statements import ( + AssertStatement, + LetStatement, + PrintStatement, + Program, + RecallStatement, + RememberStatement, + ReplyStatement, + Statement, + TraceStatement, +) + +_UNARY_OPCODE = { + UnaryOperator.POSITIVE: OpCode.UNARY_POSITIVE, + UnaryOperator.NEGATE: OpCode.UNARY_NEGATE, + UnaryOperator.NOT: OpCode.UNARY_NOT, +} +_BINARY_OPCODE = { + BinaryOperator.OR: OpCode.BINARY_OR, + BinaryOperator.AND: OpCode.BINARY_AND, + BinaryOperator.EQUAL: OpCode.BINARY_EQUAL, + BinaryOperator.NOT_EQUAL: OpCode.BINARY_NOT_EQUAL, + BinaryOperator.GREATER: OpCode.BINARY_GREATER, + BinaryOperator.GREATER_EQUAL: OpCode.BINARY_GREATER_EQUAL, + BinaryOperator.LESS: OpCode.BINARY_LESS, + BinaryOperator.LESS_EQUAL: OpCode.BINARY_LESS_EQUAL, + BinaryOperator.ADD: OpCode.BINARY_ADD, + BinaryOperator.SUBTRACT: OpCode.BINARY_SUBTRACT, + BinaryOperator.MULTIPLY: OpCode.BINARY_MULTIPLY, + BinaryOperator.DIVIDE: OpCode.BINARY_DIVIDE, +} + + +@dataclass(frozen=True, slots=True) +class IXCompiler: + """Compile accepted IX programs without evaluating or executing them.""" + + def compile_analysis(self, analysis: IXFrontendAnalysis) -> BytecodeProgram: + """Compile a front-end analysis after requiring it to be accepted.""" + analysis.require_accepted() + return self.compile_program( + analysis.program, + source_digest=analysis.source_digest, + ) + + def compile_program( + self, + program: Program, + *, + source_digest: DigestRecord, + ) -> BytecodeProgram: + """Compile a typed program into a final-halt instruction stream.""" + instructions: list[Instruction] = [] + for statement in program.statements: + self._compile_statement(statement, instructions=instructions) + instructions.append(Instruction(OpCode.HALT)) + return BytecodeProgram.create( + instructions=instructions, + source_digest=source_digest, + ) + + def _compile_statement( + self, + statement: Statement, + *, + instructions: list[Instruction], + ) -> None: + """Append bytecode for one supported statement.""" + if isinstance(statement, LetStatement): + self._compile_expression(statement.expression, instructions=instructions) + instructions.append(Instruction(OpCode.STORE_LOCAL, name=statement.name)) + return + if isinstance(statement, RememberStatement): + self._compile_expression(statement.expression, instructions=instructions) + instructions.append(Instruction(OpCode.STORE_MEMORY, name=statement.name)) + return + if isinstance(statement, RecallStatement): + instructions.append(Instruction(OpCode.LOAD_MEMORY, name=statement.name)) + instructions.append(Instruction(OpCode.EMIT_OUTPUT)) + return + if isinstance(statement, PrintStatement): + self._compile_expression(statement.expression, instructions=instructions) + instructions.append(Instruction(OpCode.EMIT_OUTPUT)) + return + if isinstance(statement, ReplyStatement): + self._compile_expression(statement.expression, instructions=instructions) + instructions.append(Instruction(OpCode.EMIT_REPLY)) + return + if isinstance(statement, AssertStatement): + self._compile_expression(statement.expression, instructions=instructions) + instructions.append(Instruction(OpCode.ASSERT)) + return + if isinstance(statement, TraceStatement): + self._compile_expression(statement.expression, instructions=instructions) + instructions.append(Instruction(OpCode.TRACE)) + return + raise FoundationError( + f"unsupported IX statement for compilation: {type(statement).__name__}" + ) + + def _compile_expression( + self, + expression: Expression, + *, + instructions: list[Instruction], + ) -> None: + """Append stack-machine bytecode for one expression.""" + if isinstance(expression, LiteralExpression): + instructions.append( + Instruction( + OpCode.PUSH, + value=CognitiveValue.from_python(expression.value), + ) + ) + return + if isinstance(expression, NameExpression): + instructions.append(Instruction(OpCode.LOAD_LOCAL, name=expression.name)) + return + if isinstance(expression, GroupExpression): + self._compile_expression(expression.expression, instructions=instructions) + return + if isinstance(expression, UnaryExpression): + self._compile_expression(expression.operand, instructions=instructions) + instructions.append(Instruction(_UNARY_OPCODE[expression.operator])) + return + if isinstance(expression, BinaryExpression): + self._compile_expression(expression.left, instructions=instructions) + self._compile_expression(expression.right, instructions=instructions) + instructions.append(Instruction(_BINARY_OPCODE[expression.operator])) + return + raise FoundationError( + f"unsupported IX expression for compilation: {type(expression).__name__}" + ) + + +def compile_ix_source( + source: str, + *, + filename: str = "", +) -> BytecodeProgram: + """Analyze and compile one IX source document.""" + return IXCompiler().compile_analysis(require_accepted_ix_source(source, filename=filename)) diff --git a/src/ix_sally/cognition/curriculum.py b/src/ix_sally/cognition/curriculum.py new file mode 100644 index 0000000..9af2ef9 --- /dev/null +++ b/src/ix_sally/cognition/curriculum.py @@ -0,0 +1,327 @@ +"""Deterministic curricula, held-out tasks, and progression evidence.""" + +from __future__ import annotations + +from collections.abc import Iterable +from dataclasses import dataclass +from enum import StrEnum + +from ix_sally.digest import DigestRecord, JsonArray, JsonObject +from ix_sally.foundation import CanonicalKey, FoundationError, require_text + + +class CurriculumSplit(StrEnum): + """Task split used to distinguish practice from transfer evaluation.""" + + TRAINING = "training" + VALIDATION = "validation" + HELD_OUT = "held_out" + + +class TrialStatus(StrEnum): + """Observed result of one curriculum task attempt.""" + + PASSED = "passed" + PARTIAL = "partial" + FAILED = "failed" + BLOCKED = "blocked" + + +@dataclass(frozen=True, slots=True) +class CurriculumTask: + """One bounded task with explicit prerequisites and evaluation split.""" + + task_id: CanonicalKey + family: CanonicalKey + description: str + difficulty: int + split: CurriculumSplit + prerequisite_ids: tuple[CanonicalKey, ...] = () + required_capabilities: tuple[CanonicalKey, ...] = () + pass_threshold: float = 0.8 + + @classmethod + def create( + cls, + *, + task_id: str, + family: str, + description: str, + difficulty: int, + split: CurriculumSplit, + prerequisite_ids: Iterable[str] = (), + required_capabilities: Iterable[str] = (), + pass_threshold: float = 0.8, + ) -> CurriculumTask: + """Create a curriculum task with measurable success criteria.""" + if not 1 <= difficulty <= 10: + raise FoundationError("curriculum difficulty must be between 1 and 10") + if not 0.0 <= pass_threshold <= 1.0: + raise FoundationError("curriculum pass threshold must be between 0 and 1") + canonical_id = CanonicalKey.from_text(task_id, field_name="task_id") + prerequisites = tuple( + sorted( + { + CanonicalKey.from_text(item, field_name="prerequisite_id") + for item in prerequisite_ids + }, + key=lambda item: item.value, + ) + ) + if canonical_id in prerequisites: + raise FoundationError("curriculum task must not depend on itself") + capabilities = tuple( + sorted( + { + CanonicalKey.from_text(item, field_name="required_capability") + for item in required_capabilities + }, + key=lambda item: item.value, + ) + ) + return cls( + task_id=canonical_id, + family=CanonicalKey.from_text(family, field_name="family"), + description=require_text(description, field_name="description"), + difficulty=difficulty, + split=split, + prerequisite_ids=prerequisites, + required_capabilities=capabilities, + pass_threshold=pass_threshold, + ) + + def to_payload(self) -> JsonObject: + """Return a canonical task payload.""" + prerequisites: JsonArray = [item.value for item in self.prerequisite_ids] + capabilities: JsonArray = [item.value for item in self.required_capabilities] + return { + "task_id": self.task_id.value, + "family": self.family.value, + "description": self.description, + "difficulty": self.difficulty, + "split": self.split.value, + "prerequisite_ids": prerequisites, + "required_capabilities": capabilities, + "pass_threshold": self.pass_threshold, + } + + def digest(self) -> DigestRecord: + """Return a deterministic task identity.""" + return DigestRecord.from_payload(self.to_payload()) + + +@dataclass(frozen=True, slots=True) +class Curriculum: + """Immutable acyclic graph of measurable tasks.""" + + tasks: tuple[CurriculumTask, ...] + + @classmethod + def create(cls, tasks: Iterable[CurriculumTask]) -> Curriculum: + """Create a curriculum and validate all prerequisites and splits.""" + normalized = tuple(sorted(tasks, key=lambda item: item.task_id.value)) + if not normalized: + raise FoundationError("curriculum requires at least one task") + identifiers = [item.task_id.value for item in normalized] + if len(identifiers) != len(set(identifiers)): + raise FoundationError("curriculum contains duplicate task identifiers") + known = set(identifiers) + for task in normalized: + missing = sorted( + item.value for item in task.prerequisite_ids if item.value not in known + ) + if missing: + raise FoundationError( + f"task {task.task_id.value} has unknown prerequisites: {', '.join(missing)}" + ) + curriculum = cls(normalized) + curriculum._require_acyclic() + return curriculum + + def _require_acyclic(self) -> None: + """Reject task prerequisite cycles.""" + by_id = {task.task_id.value: task for task in self.tasks} + visiting: set[str] = set() + visited: set[str] = set() + + def visit(task_id: str) -> None: + if task_id in visited: + return + if task_id in visiting: + raise FoundationError(f"curriculum dependency cycle detected at {task_id}") + visiting.add(task_id) + for dependency in by_id[task_id].prerequisite_ids: + visit(dependency.value) + visiting.remove(task_id) + visited.add(task_id) + + for task_id in sorted(by_id): + visit(task_id) + + def require(self, task_id: str) -> CurriculumTask: + """Return one task by canonical identifier.""" + requested = CanonicalKey.from_text(task_id, field_name="task_id") + for task in self.tasks: + if task.task_id == requested: + return task + raise FoundationError(f"unknown curriculum task: {requested.value}") + + def by_split(self, split: CurriculumSplit) -> tuple[CurriculumTask, ...]: + """Return tasks assigned to one evaluation split.""" + return tuple(task for task in self.tasks if task.split is split) + + def to_payload(self) -> JsonObject: + """Return a canonical curriculum payload.""" + tasks: JsonArray = [task.to_payload() for task in self.tasks] + return {"tasks": tasks} + + def digest(self) -> DigestRecord: + """Return a deterministic curriculum identity.""" + return DigestRecord.from_payload(self.to_payload()) + + +@dataclass(frozen=True, slots=True) +class CurriculumTrial: + """One evidence-bound observed attempt on a curriculum task.""" + + trial_id: CanonicalKey + task_id: CanonicalKey + sequence: int + score: float + status: TrialStatus + evidence_digest: DigestRecord + notes: str + + @classmethod + def create( + cls, + *, + trial_id: str, + task_id: str, + sequence: int, + score: float, + status: TrialStatus, + evidence_digest: DigestRecord, + notes: str, + ) -> CurriculumTrial: + """Create a task trial from an observed result.""" + if sequence < 0: + raise FoundationError("curriculum trial sequence must not be negative") + if not 0.0 <= score <= 1.0: + raise FoundationError("curriculum trial score must be between 0 and 1") + evidence_digest.require_algorithm("sha256") + return cls( + trial_id=CanonicalKey.from_text(trial_id, field_name="trial_id"), + task_id=CanonicalKey.from_text(task_id, field_name="task_id"), + sequence=sequence, + score=score, + status=status, + evidence_digest=evidence_digest, + notes=require_text(notes, field_name="notes"), + ) + + def to_payload(self) -> JsonObject: + """Return a canonical trial payload.""" + return { + "trial_id": self.trial_id.value, + "task_id": self.task_id.value, + "sequence": self.sequence, + "score": self.score, + "status": self.status.value, + "evidence_digest": { + "algorithm": self.evidence_digest.algorithm, + "value": self.evidence_digest.value, + }, + "notes": self.notes, + } + + def digest(self) -> DigestRecord: + """Return a deterministic trial identity.""" + return DigestRecord.from_payload(self.to_payload()) + + +@dataclass(frozen=True, slots=True) +class CurriculumLedger: + """Immutable curriculum trial history with progression and transfer queries.""" + + curriculum: Curriculum + trials: tuple[CurriculumTrial, ...] = () + + def __post_init__(self) -> None: + """Require known tasks, unique trials, and contiguous sequence numbers.""" + identifiers = [trial.trial_id.value for trial in self.trials] + if len(identifiers) != len(set(identifiers)): + raise FoundationError("curriculum ledger contains duplicate trials") + known = {task.task_id for task in self.curriculum.tasks} + for index, trial in enumerate(self.trials): + if trial.sequence != index: + raise FoundationError("curriculum trial sequence must be contiguous") + if trial.task_id not in known: + raise FoundationError(f"trial references unknown task: {trial.task_id.value}") + + def record(self, trial: CurriculumTrial) -> CurriculumLedger: + """Append one trial when its next sequence number is exact.""" + return CurriculumLedger(self.curriculum, (*self.trials, trial)) + + def trials_for(self, task_id: str) -> tuple[CurriculumTrial, ...]: + """Return all trials for one task.""" + task = self.curriculum.require(task_id) + return tuple(trial for trial in self.trials if trial.task_id == task.task_id) + + def task_passed(self, task: CurriculumTask) -> bool: + """Return whether any observed trial meets the task threshold and status.""" + return any( + trial.status is TrialStatus.PASSED and trial.score >= task.pass_threshold + for trial in self.trials + if trial.task_id == task.task_id + ) + + def eligible_tasks(self) -> tuple[CurriculumTask, ...]: + """Return incomplete tasks whose prerequisites are already passed.""" + by_id = {task.task_id: task for task in self.curriculum.tasks} + eligible = [] + for task in self.curriculum.tasks: + if self.task_passed(task): + continue + if all(self.task_passed(by_id[item]) for item in task.prerequisite_ids): + eligible.append(task) + return tuple( + sorted( + eligible, + key=lambda item: (item.difficulty, item.task_id.value), + ) + ) + + def split_score(self, split: CurriculumSplit) -> float: + """Return mean best-task score for one split, or zero when untested.""" + tasks = self.curriculum.by_split(split) + scores = [] + for task in tasks: + task_trials = self.trials_for(task.task_id.value) + if task_trials: + scores.append(max(trial.score for trial in task_trials)) + return round(sum(scores) / len(scores), 12) if scores else 0.0 + + def transfer_gap(self) -> float: + """Return validation-to-held-out score drop without hiding negative transfer.""" + return round( + self.split_score(CurriculumSplit.VALIDATION) + - self.split_score(CurriculumSplit.HELD_OUT), + 12, + ) + + def to_payload(self) -> JsonObject: + """Return a canonical curriculum-ledger payload.""" + trials: JsonArray = [trial.to_payload() for trial in self.trials] + return { + "curriculum": self.curriculum.to_payload(), + "trials": trials, + "training_score": self.split_score(CurriculumSplit.TRAINING), + "validation_score": self.split_score(CurriculumSplit.VALIDATION), + "held_out_score": self.split_score(CurriculumSplit.HELD_OUT), + "transfer_gap": self.transfer_gap(), + } + + def digest(self) -> DigestRecord: + """Return a deterministic curriculum-ledger identity.""" + return DigestRecord.from_payload(self.to_payload()) diff --git a/src/ix_sally/cognition/episodes.py b/src/ix_sally/cognition/episodes.py new file mode 100644 index 0000000..af0683b --- /dev/null +++ b/src/ix_sally/cognition/episodes.py @@ -0,0 +1,217 @@ +"""Replayable cognitive episodes with an append-only digest chain.""" + +from __future__ import annotations + +from collections.abc import Iterable +from dataclasses import dataclass +from enum import StrEnum + +from ix_sally.digest import DigestRecord, JsonArray, JsonObject +from ix_sally.foundation import CanonicalKey, FoundationError, require_text + + +class EpisodeStepKind(StrEnum): + """Closed step categories recorded during a cognitive episode.""" + + INPUT = "input" + ATTENTION = "attention" + RETRIEVAL = "retrieval" + INFERENCE = "inference" + PLANNING = "planning" + AUTHORITY = "authority" + EXECUTION = "execution" + LEARNING = "learning" + OUTPUT = "output" + + +class EpisodeStepStatus(StrEnum): + """Result status for one episode step.""" + + COMPLETED = "completed" + BLOCKED = "blocked" + FAILED = "failed" + SKIPPED = "skipped" + + +@dataclass(frozen=True, slots=True) +class EpisodeStep: + """One ordered, digest-bound event within a cognitive episode.""" + + index: int + kind: EpisodeStepKind + status: EpisodeStepStatus + detail: str + input_digests: tuple[DigestRecord, ...] = () + output_digests: tuple[DigestRecord, ...] = () + + @classmethod + def create( + cls, + *, + index: int, + kind: EpisodeStepKind, + status: EpisodeStepStatus, + detail: str, + input_digests: Iterable[DigestRecord] = (), + output_digests: Iterable[DigestRecord] = (), + ) -> EpisodeStep: + """Create one non-negative episode step with SHA-256 references.""" + if index < 0: + raise FoundationError("episode step index must not be negative") + inputs = tuple(input_digests) + outputs = tuple(output_digests) + for digest in (*inputs, *outputs): + digest.require_algorithm("sha256") + return cls( + index=index, + kind=kind, + status=status, + detail=require_text(detail, field_name="detail"), + input_digests=inputs, + output_digests=outputs, + ) + + def to_payload(self) -> JsonObject: + """Return a canonical step payload.""" + inputs: JsonArray = [ + {"algorithm": item.algorithm, "value": item.value} for item in self.input_digests + ] + outputs: JsonArray = [ + {"algorithm": item.algorithm, "value": item.value} for item in self.output_digests + ] + return { + "index": self.index, + "kind": self.kind.value, + "status": self.status.value, + "detail": self.detail, + "input_digests": inputs, + "output_digests": outputs, + } + + def digest(self) -> DigestRecord: + """Return a deterministic step identity.""" + return DigestRecord.from_payload(self.to_payload()) + + +@dataclass(frozen=True, slots=True) +class CognitiveEpisode: + """One complete cognitive episode linked to the prior episode digest.""" + + episode_id: CanonicalKey + sequence: int + task: str + initial_state_digest: DigestRecord + final_state_digest: DigestRecord + steps: tuple[EpisodeStep, ...] + previous_episode_digest: DigestRecord | None = None + + @classmethod + def create( + cls, + *, + episode_id: str, + sequence: int, + task: str, + initial_state_digest: DigestRecord, + final_state_digest: DigestRecord, + steps: Iterable[EpisodeStep], + previous_episode_digest: DigestRecord | None = None, + ) -> CognitiveEpisode: + """Create a replayable episode with contiguous step indexes.""" + if sequence < 0: + raise FoundationError("episode sequence must not be negative") + initial_state_digest.require_algorithm("sha256") + final_state_digest.require_algorithm("sha256") + if previous_episode_digest is not None: + previous_episode_digest.require_algorithm("sha256") + normalized_steps = tuple(steps) + expected_indexes = tuple(range(len(normalized_steps))) + actual_indexes = tuple(step.index for step in normalized_steps) + if actual_indexes != expected_indexes: + raise FoundationError("episode step indexes must be contiguous from zero") + return cls( + episode_id=CanonicalKey.from_text(episode_id, field_name="episode_id"), + sequence=sequence, + task=require_text(task, field_name="task"), + initial_state_digest=initial_state_digest, + final_state_digest=final_state_digest, + steps=normalized_steps, + previous_episode_digest=previous_episode_digest, + ) + + def completed(self) -> bool: + """Return whether every recorded step completed successfully.""" + return all(step.status is EpisodeStepStatus.COMPLETED for step in self.steps) + + def to_payload(self) -> JsonObject: + """Return a canonical episode payload.""" + steps: JsonArray = [step.to_payload() for step in self.steps] + previous: JsonObject | None = None + if self.previous_episode_digest is not None: + previous = { + "algorithm": self.previous_episode_digest.algorithm, + "value": self.previous_episode_digest.value, + } + return { + "episode_id": self.episode_id.value, + "sequence": self.sequence, + "task": self.task, + "initial_state_digest": { + "algorithm": self.initial_state_digest.algorithm, + "value": self.initial_state_digest.value, + }, + "final_state_digest": { + "algorithm": self.final_state_digest.algorithm, + "value": self.final_state_digest.value, + }, + "steps": steps, + "previous_episode_digest": previous, + "completed": self.completed(), + } + + def digest(self) -> DigestRecord: + """Return a deterministic episode identity.""" + return DigestRecord.from_payload(self.to_payload()) + + +@dataclass(frozen=True, slots=True) +class EpisodeLedger: + """Immutable, append-only chain of complete cognitive episodes.""" + + episodes: tuple[CognitiveEpisode, ...] = () + + @classmethod + def create(cls, episodes: Iterable[CognitiveEpisode] = ()) -> EpisodeLedger: + """Create and verify an episode chain.""" + normalized = tuple(episodes) + identifiers = [item.episode_id.value for item in normalized] + if len(identifiers) != len(set(identifiers)): + raise FoundationError("episode ledger contains duplicate identifiers") + for index, episode in enumerate(normalized): + if episode.sequence != index: + raise FoundationError("episode sequence must be contiguous from zero") + expected_previous = None if index == 0 else normalized[index - 1].digest() + if episode.previous_episode_digest != expected_previous: + raise FoundationError("episode previous digest does not match ledger head") + return cls(normalized) + + def append(self, episode: CognitiveEpisode) -> EpisodeLedger: + """Append an episode only when its sequence and chain link are exact.""" + return EpisodeLedger.create((*self.episodes, episode)) + + def next_sequence(self) -> int: + """Return the sequence number required by the next episode.""" + return len(self.episodes) + + def head_digest(self) -> DigestRecord | None: + """Return the current episode-chain head.""" + return self.episodes[-1].digest() if self.episodes else None + + def to_payload(self) -> JsonObject: + """Return a canonical ledger payload.""" + episodes: JsonArray = [episode.to_payload() for episode in self.episodes] + return {"episodes": episodes} + + def digest(self) -> DigestRecord: + """Return a deterministic ledger identity.""" + return DigestRecord.from_payload(self.to_payload()) diff --git a/src/ix_sally/cognition/evaluation.py b/src/ix_sally/cognition/evaluation.py new file mode 100644 index 0000000..ecbc749 --- /dev/null +++ b/src/ix_sally/cognition/evaluation.py @@ -0,0 +1,710 @@ +"""Reproducible capability evaluation for the IX-Sally cognitive runtime.""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import StrEnum + +from ix_sally.agents import AgentRole +from ix_sally.cognition.active_memory import ( + ActiveMemoryEntry, + ActiveMemoryStatus, + MemoryLayer, +) +from ix_sally.cognition.adaptation import AdaptationController +from ix_sally.cognition.curriculum import ( + Curriculum, + CurriculumLedger, + CurriculumSplit, + CurriculumTask, + CurriculumTrial, + TrialStatus, +) +from ix_sally.cognition.episodes import ( + CognitiveEpisode, + EpisodeStep, + EpisodeStepKind, + EpisodeStepStatus, +) +from ix_sally.cognition.executive import ExecutiveDecisionStatus +from ix_sally.cognition.goals import GoalSpec +from ix_sally.cognition.learning import ( + LearningOutcome, + OutcomeStatus, + TransferEvaluation, +) +from ix_sally.cognition.metacognition import CapabilityMeasure, SelfModel +from ix_sally.cognition.planning import ActionSpec, FactEffect, PlanStatus +from ix_sally.cognition.system import SallyCognitiveSystem +from ix_sally.cognition.uncertainty import CalibrationObservation +from ix_sally.cognition.values import CognitiveValue +from ix_sally.cognition.vm import VMStatus +from ix_sally.cognition.workspace import WorkspaceItem, WorkspaceItemKind +from ix_sally.cognition.world_model import ( + CausalRule, + FactPattern, + FactStatus, + WorldFact, +) +from ix_sally.digest import DigestRecord, JsonArray, JsonObject +from ix_sally.foundation import CanonicalKey, FoundationError, require_text + + +class EvaluationCategory(StrEnum): + """Capability categories measured by the built-in evaluation harness.""" + + LANGUAGE = "language" + EXECUTION = "execution" + MEMORY = "memory" + WORLD_MODEL = "world_model" + PLANNING = "planning" + LEARNING = "learning" + GOVERNANCE = "governance" + PERSISTENCE = "persistence" + INTEGRATION = "integration" + + +@dataclass(frozen=True, slots=True) +class BenchmarkResult: + """One observed benchmark result with an evidence digest.""" + + benchmark_id: CanonicalKey + category: EvaluationCategory + passed: bool + score: float + detail: str + evidence_digest: DigestRecord + + @classmethod + def create( + cls, + *, + benchmark_id: str, + category: EvaluationCategory, + passed: bool, + score: float, + detail: str, + evidence_digest: DigestRecord, + ) -> BenchmarkResult: + """Create a bounded result from actual benchmark observations.""" + if not 0.0 <= score <= 1.0: + raise FoundationError("benchmark score must be between 0 and 1") + evidence_digest.require_algorithm("sha256") + return cls( + benchmark_id=CanonicalKey.from_text( + benchmark_id, + field_name="benchmark_id", + ), + category=category, + passed=passed, + score=score, + detail=require_text(detail, field_name="detail"), + evidence_digest=evidence_digest, + ) + + def to_payload(self) -> JsonObject: + """Return a canonical benchmark-result payload.""" + return { + "benchmark_id": self.benchmark_id.value, + "category": self.category.value, + "passed": self.passed, + "score": self.score, + "detail": self.detail, + "evidence_digest": { + "algorithm": self.evidence_digest.algorithm, + "value": self.evidence_digest.value, + }, + } + + +@dataclass(frozen=True, slots=True) +class CognitiveEvaluationReport: + """Complete evaluation report that explicitly does not certify AGI.""" + + results: tuple[BenchmarkResult, ...] + classification: str = "experimental-cognitive-architecture" + agi_certified: bool = False + + def __post_init__(self) -> None: + """Reject duplicate benchmarks and any self-certification claim.""" + identifiers = [result.benchmark_id.value for result in self.results] + if len(identifiers) != len(set(identifiers)): + raise FoundationError("evaluation contains duplicate benchmark identifiers") + if self.agi_certified: + raise FoundationError("IX-Sally may not self-certify AGI") + require_text(self.classification, field_name="classification") + + def passed(self) -> int: + """Return the number of passed benchmarks.""" + return sum(1 for result in self.results if result.passed) + + def overall_score(self) -> float: + """Return the unweighted mean benchmark score.""" + if not self.results: + return 0.0 + return round(sum(result.score for result in self.results) / len(self.results), 12) + + def category_scores(self) -> dict[str, float]: + """Return deterministic mean scores grouped by capability category.""" + scores: dict[str, float] = {} + for category in EvaluationCategory: + relevant = [result.score for result in self.results if result.category is category] + if relevant: + scores[category.value] = round(sum(relevant) / len(relevant), 12) + return scores + + def to_payload(self) -> JsonObject: + """Return a canonical evaluation-report payload.""" + results: JsonArray = [result.to_payload() for result in self.results] + category_scores: JsonObject = {} + for category, score in self.category_scores().items(): + category_scores[category] = score + return { + "classification": self.classification, + "agi_certified": self.agi_certified, + "benchmark_count": len(self.results), + "passed_count": self.passed(), + "overall_score": self.overall_score(), + "category_scores": category_scores, + "results": results, + "limitations": [ + "Passing this suite does not establish artificial general intelligence.", + "The suite is deterministic and local; it does not measure open-world autonomy.", + "No external model weights, sensors, actuators, or proprietary data are included.", + ], + } + + def digest(self) -> DigestRecord: + """Return a deterministic evaluation-report identity.""" + return DigestRecord.from_payload(self.to_payload()) + + +def _result( + benchmark_id: str, + category: EvaluationCategory, + passed: bool, + detail: str, + evidence: JsonObject, +) -> BenchmarkResult: + """Create a benchmark result with a binary observed score.""" + return BenchmarkResult.create( + benchmark_id=benchmark_id, + category=category, + passed=passed, + score=1.0 if passed else 0.0, + detail=detail, + evidence_digest=DigestRecord.from_payload(evidence), + ) + + +def run_core_evaluation() -> CognitiveEvaluationReport: + """Execute the built-in deterministic capability suite against a fresh system.""" + system = SallyCognitiveSystem.create() + results: list[BenchmarkResult] = [] + + arithmetic = system.execute_ix( + "let total = 6 * 7\nprint total\nassert total == 42\n", + filename="evaluation-arithmetic.ix", + ) + arithmetic_passed = arithmetic.status is VMStatus.HALTED and arithmetic.outputs == ( + CognitiveValue.from_python(42), + ) + results.append( + _result( + "ix-typed-arithmetic", + EvaluationCategory.LANGUAGE, + arithmetic_passed, + "Compiled and executed typed arithmetic with a runtime assertion.", + arithmetic.to_payload(), + ) + ) + + memory_run = system.execute_ix( + "remember answer = 42\nrecall answer\n", + filename="evaluation-memory.ix", + ) + memory_passed = ( + memory_run.status is VMStatus.HALTED + and memory_run.outputs == (CognitiveValue.from_python(42),) + and system.runtime_memories.get("answer") == CognitiveValue.from_python(42) + ) + results.append( + _result( + "vm-governed-memory", + EvaluationCategory.EXECUTION, + memory_passed, + "Stored and recalled an exact typed VM memory value.", + memory_run.to_payload(), + ) + ) + + primitive = system.execute_primitive( + "add-two", + (CognitiveValue.from_python(20), CognitiveValue.from_python(22)), + ) + primitive_passed = primitive.output == CognitiveValue.from_python(42) + results.append( + _result( + "grounded-primitive", + EvaluationCategory.EXECUTION, + primitive_passed, + "Executed an enumerated validated primitive without a dynamic callback.", + primitive.to_payload(), + ) + ) + + evidence = DigestRecord.from_payload({"observation": "sky appears blue"}) + system.append_memory( + ActiveMemoryEntry.create( + memory_id="blue-sky-observation", + layer=MemoryLayer.SEMANTIC, + content="The observed daytime sky appears blue.", + confidence=0.95, + status=ActiveMemoryStatus.VERIFIED, + sequence=1, + evidence_digests=(evidence,), + tags=("sky", "color"), + ) + ) + retrievals = system.active_memory.retrieve("daytime sky color", truth_only=True) + retrieval_passed = bool( + retrievals and retrievals[0].entry.memory_id.value == "blue-sky-observation" + ) + results.append( + _result( + "active-memory-retrieval", + EvaluationCategory.MEMORY, + retrieval_passed, + "Retrieved verified semantic memory using transparent lexical scoring.", + system.active_memory.to_payload(), + ) + ) + + observation_digest = DigestRecord.from_payload({"sensor": "temperature", "value": 90}) + system.observe( + WorldFact.create( + fact_id="room-temperature-hot", + subject="room", + predicate="temperature-state", + value=CognitiveValue.from_python("hot"), + status=FactStatus.OBSERVED, + confidence=1.0, + evidence_digests=(observation_digest,), + ) + ) + system.add_causal_rule( + CausalRule.create( + rule_id="hot-room-needs-cooling", + conditions=( + FactPattern.create( + subject="room", + predicate="temperature-state", + value=CognitiveValue.from_python("hot"), + ), + ), + effect_subject="room", + effect_predicate="cooling-needed", + effect_value=CognitiveValue.from_python(True), + confidence=0.9, + evidence_digests=(observation_digest,), + ) + ) + predictions = system.world_model.predict() + prediction_passed = bool( + predictions + and predictions[0].predicate.value == "cooling-needed" + and predictions[0].status is FactStatus.PREDICTED + ) + results.append( + _result( + "causal-prediction", + EvaluationCategory.WORLD_MODEL, + prediction_passed, + "Produced a prediction while preserving its predicted epistemic status.", + system.world_model.to_payload(), + ) + ) + + system.register_action( + ActionSpec.create( + action_id="activate-cooling", + description="Activate the simulated cooling state.", + preconditions=( + FactPattern.create( + subject="room", + predicate="cooling-needed", + value=CognitiveValue.from_python(True), + ), + ), + effects=( + FactEffect.create( + subject="room", + predicate="temperature-state", + value=CognitiveValue.from_python("comfortable"), + ), + ), + cost=1.0, + risk=0.05, + ) + ) + system.infer_world() + goal = FactPattern.create( + subject="room", + predicate="temperature-state", + value=CognitiveValue.from_python("comfortable"), + ) + plan = system.plan(goal) + plan_passed = plan.status is PlanStatus.FOUND and tuple( + action.action_id.value for action in plan.actions + ) == ("activate-cooling",) + results.append( + _result( + "bounded-planning", + EvaluationCategory.PLANNING, + plan_passed, + "Found a shortest exact-state plan using declarative actions.", + plan.to_payload(), + ) + ) + + learning_evidence: list[DigestRecord] = [] + for index, score in enumerate((0.6, 0.75, 0.9, 0.9), start=1): + outcome = LearningOutcome.create( + outcome_id=f"planning-outcome-{index}", + skill_id="bounded-planning", + task_family="state-transition", + status=OutcomeStatus.SUCCESS if score >= 0.8 else OutcomeStatus.PARTIAL, + score=score, + evidence_digest=DigestRecord.from_payload({"trial": index, "observed_score": score}), + notes="Deterministic held-out state-transition trial.", + ) + learning_evidence.append(outcome.digest()) + system.record_learning(outcome) + transfer = TransferEvaluation.create( + skill_id="bounded-planning", + familiar_score=0.9, + novel_score=0.8, + retention_score=system.learning.retention_score("bounded-planning"), + evidence_digests=learning_evidence, + ) + results.append( + _result( + "measured-transfer", + EvaluationCategory.LEARNING, + transfer.passes(), + "Measured familiar, held-out, and retention scores with declared thresholds.", + transfer.to_payload(), + ) + ) + + system.admit_workspace( + WorkspaceItem.create( + item_id="goal-comfortable-room", + kind=WorkspaceItemKind.GOAL, + content="Reach a comfortable simulated room temperature.", + confidence=1.0, + salience=0.9, + evidence_digests=(plan.digest(),), + ) + ) + cycle = system.run_cycle(task="Make the room comfortable", goal=goal) + ninefold_passed = len(cycle.findings) == len(AgentRole) and { + finding.role for finding in cycle.findings + } == set(AgentRole) + results.append( + _result( + "functional-ninefold-cycle", + EvaluationCategory.INTEGRATION, + ninefold_passed, + "Executed all nine cognitive functions exactly once without role overlap.", + cycle.to_payload(), + ) + ) + + authority_action = ActionSpec.create( + action_id="human-boundary-action", + description="A simulated action that requires explicit human approval.", + preconditions=(), + effects=( + FactEffect.create( + subject="system", + predicate="external-change", + value=CognitiveValue.from_python(True), + ), + ), + cost=0.0, + risk=0.2, + authority_required=True, + ) + authority_system = SallyCognitiveSystem.create() + authority_system.register_action(authority_action) + authority_goal = FactPattern.create( + subject="system", + predicate="external-change", + value=CognitiveValue.from_python(True), + ) + authority_plan = authority_system.plan(authority_goal) + blocked_receipt = authority_system.simulate_plan(authority_plan) + governance_passed = blocked_receipt.permission.value == "requires_human" + results.append( + _result( + "human-authority-boundary", + EvaluationCategory.GOVERNANCE, + governance_passed, + "Blocked a declared authority-crossing plan without human approval.", + blocked_receipt.to_payload(), + ) + ) + + system.record_calibration( + CalibrationObservation.create( + observation_id="planning-forecast-success", + capability_id="planning", + predicted_probability=0.8, + observed=True, + evidence_digest=DigestRecord.from_payload( + {"forecast": "planning-success", "observed": True} + ), + context="Deterministic planning forecast.", + ) + ) + system.record_calibration( + CalibrationObservation.create( + observation_id="planning-forecast-failure", + capability_id="planning", + predicted_probability=0.2, + observed=False, + evidence_digest=DigestRecord.from_payload( + {"forecast": "planning-failure", "observed": False} + ), + context="Deterministic planning forecast.", + ) + ) + calibration = system.calibration_report(capability_id="planning", bin_count=5) + calibration_passed = ( + calibration.observation_count == 2 + and calibration.brier_score == 0.04 + and calibration.expected_calibration_error == 0.2 + ) + results.append( + _result( + "calibrated-uncertainty", + EvaluationCategory.LEARNING, + calibration_passed, + "Measured Brier score and calibration error from explicit forecasts.", + calibration.to_payload(), + ) + ) + + system.register_goal( + GoalSpec.create( + goal_id="comfortable-room", + description="Reach a comfortable simulated room temperature.", + desired_state=goal, + priority=1.0, + utility=1.0, + risk_limit=0.2, + evidence_digests=(plan.digest(),), + ) + ) + decision = system.deliberate(task="Make the simulated room comfortable.") + bridge = system.bridge_decision(decision, cycle=system.cycle_count) + executive_passed = ( + decision.status is ExecutiveDecisionStatus.PLAN_READY + and bridge.proposal.proposed_actions[0].action_id.value == "activate-cooling" + and bridge.receipt.executive_decision_digest == decision.digest() + ) + results.append( + _result( + "executive-governance-bridge", + EvaluationCategory.GOVERNANCE, + executive_passed, + "Selected a goal and converted its plan into the existing proposal path.", + bridge.to_payload(), + ) + ) + + training_task = CurriculumTask.create( + task_id="planning-training", + family="planning", + description="Complete a familiar bounded state-transition task.", + difficulty=2, + split=CurriculumSplit.TRAINING, + required_capabilities=("planning",), + pass_threshold=0.75, + ) + validation_task = CurriculumTask.create( + task_id="planning-validation", + family="planning", + description="Complete a distinct bounded validation task.", + difficulty=3, + split=CurriculumSplit.VALIDATION, + prerequisite_ids=("planning-training",), + required_capabilities=("planning",), + pass_threshold=0.75, + ) + held_out_task = CurriculumTask.create( + task_id="planning-held-out", + family="planning", + description="Complete a novel bounded state-transition task.", + difficulty=4, + split=CurriculumSplit.HELD_OUT, + prerequisite_ids=("planning-validation",), + required_capabilities=("planning", "transfer"), + pass_threshold=0.75, + ) + system.set_curriculum( + CurriculumLedger(Curriculum.create((training_task, validation_task, held_out_task))) + ) + system.record_curriculum_trial( + CurriculumTrial.create( + trial_id="planning-training-trial", + task_id="planning-training", + sequence=0, + score=0.9, + status=TrialStatus.PASSED, + evidence_digest=DigestRecord.from_payload({"task": "planning-training", "score": 0.9}), + notes="Observed deterministic training trial.", + ) + ) + system.record_curriculum_trial( + CurriculumTrial.create( + trial_id="planning-validation-trial", + task_id="planning-validation", + sequence=1, + score=0.9, + status=TrialStatus.PASSED, + evidence_digest=DigestRecord.from_payload( + {"task": "planning-validation", "score": 0.9} + ), + notes="Observed deterministic validation trial.", + ) + ) + system.record_curriculum_trial( + CurriculumTrial.create( + trial_id="planning-held-out-trial", + task_id="planning-held-out", + sequence=2, + score=0.8, + status=TrialStatus.PASSED, + evidence_digest=DigestRecord.from_payload({"task": "planning-held-out", "score": 0.8}), + notes="Observed deterministic held-out trial.", + ) + ) + if system.curriculum is None: + raise FoundationError("evaluation curriculum was not installed") + curriculum_passed = ( + system.curriculum.split_score(CurriculumSplit.TRAINING) == 0.9 + and system.curriculum.split_score(CurriculumSplit.VALIDATION) == 0.9 + and system.curriculum.split_score(CurriculumSplit.HELD_OUT) == 0.8 + and system.curriculum.transfer_gap() == 0.1 + ) + results.append( + _result( + "held-out-curriculum", + EvaluationCategory.LEARNING, + curriculum_passed, + "Recorded separate training and held-out trials without merging splits.", + system.curriculum.to_payload(), + ) + ) + + initial_episode_state = system.digest() + episode_step = EpisodeStep.create( + index=0, + kind=EpisodeStepKind.PLANNING, + status=EpisodeStepStatus.COMPLETED, + detail="Produced and bridged a bounded plan proposal.", + input_digests=(plan.digest(),), + output_digests=(bridge.digest(),), + ) + episode = CognitiveEpisode.create( + episode_id="evaluation-episode-zero", + sequence=0, + task="Make the simulated room comfortable.", + initial_state_digest=initial_episode_state, + final_state_digest=system.digest(), + steps=(episode_step,), + ) + system.append_episode(episode) + episode_passed = system.episodes.head_digest() == episode.digest() and episode.completed() + results.append( + _result( + "replayable-episode-chain", + EvaluationCategory.MEMORY, + episode_passed, + "Recorded an append-only episode with contiguous steps and digest links.", + system.episodes.to_payload(), + ) + ) + + measure_evidence = DigestRecord.from_payload({"suite": "regression-evaluation"}) + before = SelfModel( + ( + CapabilityMeasure.create( + capability_id="memory", + score=0.8, + evidence_digests=(measure_evidence,), + limitation="Measured only by this deterministic local suite.", + ), + CapabilityMeasure.create( + capability_id="planning", + score=0.4, + evidence_digests=(measure_evidence,), + limitation="Measured only by this deterministic local suite.", + ), + ) + ) + after = SelfModel( + ( + CapabilityMeasure.create( + capability_id="memory", + score=0.5, + evidence_digests=(measure_evidence,), + limitation="Measured only by this deterministic local suite.", + ), + CapabilityMeasure.create( + capability_id="planning", + score=0.7, + evidence_digests=(measure_evidence,), + limitation="Measured only by this deterministic local suite.", + ), + ) + ) + adaptation = AdaptationController(permitted_regression=0.05) + proposal = adaptation.propose_for_weakest( + self_model=before, + description="Evaluate a bounded planning heuristic change.", + expected_benefit=0.3, + regression_risk=0.2, + supporting_evidence=(before.digest(),), + ) + regression = adaptation.compare(proposal=proposal, before=before, after=after) + regression_passed = regression.has_regression() and not regression.may_request_validation() + results.append( + _result( + "regression-aware-adaptation", + EvaluationCategory.GOVERNANCE, + regression_passed, + "Detected a cross-capability regression and blocked validation advancement.", + regression.to_payload(), + ) + ) + + snapshot = system.snapshot() + restored_system = SallyCognitiveSystem.from_snapshot(snapshot) + persistence_passed = ( + restored_system.state_payload() == system.state_payload() + and restored_system.digest() == system.digest() + ) + results.append( + _result( + "tamper-evident-complete-restore", + EvaluationCategory.PERSISTENCE, + persistence_passed, + "Restored the complete canonical cognitive state and verified exact identity.", + restored_system.state_payload(), + ) + ) + + return CognitiveEvaluationReport(tuple(results)) diff --git a/src/ix_sally/cognition/executive.py b/src/ix_sally/cognition/executive.py new file mode 100644 index 0000000..930756a --- /dev/null +++ b/src/ix_sally/cognition/executive.py @@ -0,0 +1,281 @@ +"""Bounded executive selection, planning, risk gating, and decision receipts.""" + +from __future__ import annotations + +from collections.abc import Iterable +from dataclasses import dataclass, field +from enum import StrEnum + +from ix_sally.cognition.active_memory import ActiveMemoryStore +from ix_sally.cognition.goals import GoalGraph, GoalSpec +from ix_sally.cognition.planning import ActionSpec, DeterministicPlanner, Plan, PlanStatus +from ix_sally.cognition.uncertainty import CalibrationReport +from ix_sally.cognition.workspace import CognitiveWorkspace, WorkspaceItemKind +from ix_sally.cognition.world_model import WorldModel +from ix_sally.digest import DigestRecord, JsonArray, JsonObject +from ix_sally.foundation import CanonicalKey, FoundationError, require_text + + +class ExecutiveDecisionStatus(StrEnum): + """Outcome of one bounded executive deliberation.""" + + NO_SELECTABLE_GOAL = "no_selectable_goal" + GOAL_SATISFIED = "goal_satisfied" + PLAN_READY = "plan_ready" + REQUIRES_HUMAN = "requires_human" + BLOCKED_RISK = "blocked_risk" + BLOCKED_UNCERTAINTY = "blocked_uncertainty" + PLAN_NOT_FOUND = "plan_not_found" + + +@dataclass(frozen=True, slots=True) +class ExecutiveDecision: + """One deterministic decision that proposes rather than executes a plan.""" + + decision_id: CanonicalKey + status: ExecutiveDecisionStatus + task: str + selected_goal: GoalSpec | None + plan: Plan | None + blockers: tuple[str, ...] + confidence: float + evidence_digests: tuple[DigestRecord, ...] + rationale: str + + @classmethod + def create( + cls, + *, + task: str, + status: ExecutiveDecisionStatus, + selected_goal: GoalSpec | None, + plan: Plan | None, + blockers: Iterable[str], + confidence: float, + evidence_digests: Iterable[DigestRecord], + rationale: str, + ) -> ExecutiveDecision: + """Create a decision with coherent status and evidence references.""" + if not 0.0 <= confidence <= 1.0: + raise FoundationError("executive confidence must be between 0 and 1") + normalized_blockers = tuple(require_text(item, field_name="blocker") for item in blockers) + evidence = tuple(evidence_digests) + if not evidence: + raise FoundationError("executive decision requires evidence digests") + for digest in evidence: + digest.require_algorithm("sha256") + if ( + status + in { + ExecutiveDecisionStatus.PLAN_READY, + ExecutiveDecisionStatus.REQUIRES_HUMAN, + ExecutiveDecisionStatus.BLOCKED_RISK, + } + and plan is None + ): + raise FoundationError(f"{status.value} executive decision requires a plan") + if status is ExecutiveDecisionStatus.PLAN_NOT_FOUND and ( + plan is None or plan.status not in {PlanStatus.NOT_FOUND, PlanStatus.SEARCH_LIMIT} + ): + raise FoundationError("plan-not-found decision requires a failed plan") + if status is ExecutiveDecisionStatus.NO_SELECTABLE_GOAL and selected_goal is not None: + raise FoundationError("no-selectable-goal decision must not select a goal") + seed = DigestRecord.from_payload( + { + "task": task, + "status": status.value, + "goal": (selected_goal.goal_id.value if selected_goal is not None else None), + "plan": plan.digest().value if plan is not None else None, + "evidence": [item.value for item in evidence], + } + ) + return cls( + decision_id=CanonicalKey.from_text( + f"executive-decision-{seed.value[:24]}", + field_name="decision_id", + ), + status=status, + task=require_text(task, field_name="task"), + selected_goal=selected_goal, + plan=plan, + blockers=normalized_blockers, + confidence=confidence, + evidence_digests=evidence, + rationale=require_text(rationale, field_name="rationale"), + ) + + def may_enter_governance(self) -> bool: + """Return whether this proposal may proceed to the authority control plane.""" + return self.status in { + ExecutiveDecisionStatus.PLAN_READY, + ExecutiveDecisionStatus.REQUIRES_HUMAN, + } + + def to_payload(self) -> JsonObject: + """Return a canonical executive-decision payload.""" + evidence: JsonArray = [ + {"algorithm": item.algorithm, "value": item.value} for item in self.evidence_digests + ] + blockers: JsonArray = list(self.blockers) + return { + "decision_id": self.decision_id.value, + "status": self.status.value, + "task": self.task, + "selected_goal": ( + self.selected_goal.to_payload() if self.selected_goal is not None else None + ), + "plan": self.plan.to_payload() if self.plan is not None else None, + "blockers": blockers, + "confidence": self.confidence, + "evidence_digests": evidence, + "rationale": self.rationale, + "may_enter_governance": self.may_enter_governance(), + } + + def digest(self) -> DigestRecord: + """Return a deterministic decision identity.""" + return DigestRecord.from_payload(self.to_payload()) + + +@dataclass(frozen=True, slots=True) +class ExecutiveController: + """Select one goal and produce a bounded plan proposal under explicit gates.""" + + planner: DeterministicPlanner = field(default_factory=DeterministicPlanner) + maximum_calibration_error: float = 0.25 + + def __post_init__(self) -> None: + """Require a valid uncertainty gate.""" + if not 0.0 <= self.maximum_calibration_error <= 1.0: + raise FoundationError("maximum calibration error must be between 0 and 1") + + def deliberate( + self, + *, + task: str, + goals: GoalGraph, + workspace: CognitiveWorkspace, + memory: ActiveMemoryStore, + world_model: WorldModel, + actions: Iterable[ActionSpec], + calibration: CalibrationReport | None = None, + ) -> ExecutiveDecision: + """Select and plan without executing or granting authority.""" + normalized_task = require_text(task, field_name="task") + reconciled = goals.reconcile(world_model) + selected = reconciled.select(world_model) + base_evidence = ( + reconciled.digest(), + workspace.digest(), + memory.digest(), + world_model.digest(), + ) + if selected is None: + satisfied = tuple(goal for goal in reconciled.goals if goal.status.value == "satisfied") + status = ( + ExecutiveDecisionStatus.GOAL_SATISFIED + if satisfied + else ExecutiveDecisionStatus.NO_SELECTABLE_GOAL + ) + return ExecutiveDecision.create( + task=normalized_task, + status=status, + selected_goal=None, + plan=None, + blockers=(), + confidence=1.0 if satisfied else 0.0, + evidence_digests=base_evidence, + rationale=( + "All currently eligible goals are already satisfied." + if satisfied + else "No goal currently satisfies dependency and lifecycle gates." + ), + ) + evidence: tuple[DigestRecord, ...] = base_evidence + if calibration is not None: + evidence = (*evidence, calibration.digest()) + if ( + calibration.observation_count > 0 + and calibration.expected_calibration_error > self.maximum_calibration_error + ): + return ExecutiveDecision.create( + task=normalized_task, + status=ExecutiveDecisionStatus.BLOCKED_UNCERTAINTY, + selected_goal=selected, + plan=None, + blockers=("Calibration error exceeds the configured executive threshold.",), + confidence=max( + 0.0, + 1.0 - calibration.expected_calibration_error, + ), + evidence_digests=evidence, + rationale="Planning was withheld because confidence is miscalibrated.", + ) + plan = self.planner.plan( + world_model=world_model, + actions=tuple(actions), + goal=selected.desired_state, + ) + evidence = (*evidence, plan.digest()) + if plan.status in {PlanStatus.NOT_FOUND, PlanStatus.SEARCH_LIMIT}: + return ExecutiveDecision.create( + task=normalized_task, + status=ExecutiveDecisionStatus.PLAN_NOT_FOUND, + selected_goal=selected, + plan=plan, + blockers=(plan.reason,), + confidence=0.0, + evidence_digests=evidence, + rationale="The bounded planner did not establish an executable path.", + ) + if plan.status is PlanStatus.ALREADY_SATISFIED: + return ExecutiveDecision.create( + task=normalized_task, + status=ExecutiveDecisionStatus.GOAL_SATISFIED, + selected_goal=selected, + plan=None, + blockers=(), + confidence=1.0, + evidence_digests=evidence, + rationale="The selected goal is already satisfied by current world state.", + ) + explicit_risks = tuple( + item for item in workspace.items if item.kind is WorkspaceItemKind.RISK + ) + if plan.aggregate_risk > selected.risk_limit: + return ExecutiveDecision.create( + task=normalized_task, + status=ExecutiveDecisionStatus.BLOCKED_RISK, + selected_goal=selected, + plan=plan, + blockers=( + "Plan aggregate risk exceeds the selected goal risk limit.", + *(item.content for item in explicit_risks), + ), + confidence=max(0.0, 1.0 - plan.aggregate_risk), + evidence_digests=evidence, + rationale="The plan is withheld because its declared risk is too high.", + ) + requires_human = selected.authority_required or plan.requires_human_authority() + return ExecutiveDecision.create( + task=normalized_task, + status=( + ExecutiveDecisionStatus.REQUIRES_HUMAN + if requires_human + else ExecutiveDecisionStatus.PLAN_READY + ), + selected_goal=selected, + plan=plan, + blockers=( + ("Explicit human authority is required before execution.",) + if requires_human + else () + ), + confidence=max(0.0, 1.0 - plan.aggregate_risk), + evidence_digests=evidence, + rationale=( + "A bounded plan is ready for human authority review." + if requires_human + else "A bounded plan is ready to enter the existing governance pipeline." + ), + ) diff --git a/src/ix_sally/cognition/goals.py b/src/ix_sally/cognition/goals.py new file mode 100644 index 0000000..3d6af68 --- /dev/null +++ b/src/ix_sally/cognition/goals.py @@ -0,0 +1,265 @@ +"""Goal graphs, dependency resolution, and explicit executive-state transitions.""" + +from __future__ import annotations + +from collections.abc import Iterable +from dataclasses import dataclass +from enum import StrEnum + +from ix_sally.cognition.world_model import FactPattern, WorldModel +from ix_sally.digest import DigestRecord, JsonArray, JsonObject +from ix_sally.foundation import CanonicalKey, FoundationError, require_optional_text, require_text + + +class GoalStatus(StrEnum): + """Lifecycle state of one cognitive goal.""" + + PROPOSED = "proposed" + ACTIVE = "active" + BLOCKED = "blocked" + SATISFIED = "satisfied" + ABANDONED = "abandoned" + + +@dataclass(frozen=True, slots=True) +class GoalSpec: + """One bounded goal with dependencies, utility, risk, and authority metadata.""" + + goal_id: CanonicalKey + description: str + desired_state: FactPattern + priority: float + utility: float + risk_limit: float + status: GoalStatus + dependency_ids: tuple[CanonicalKey, ...] = () + authority_required: bool = False + evidence_digests: tuple[DigestRecord, ...] = () + status_reason: str | None = None + + @classmethod + def create( + cls, + *, + goal_id: str, + description: str, + desired_state: FactPattern, + priority: float, + utility: float, + risk_limit: float, + status: GoalStatus = GoalStatus.PROPOSED, + dependency_ids: Iterable[str] = (), + authority_required: bool = False, + evidence_digests: Iterable[DigestRecord] = (), + status_reason: str | None = None, + ) -> GoalSpec: + """Create a goal without inferring authority or satisfaction.""" + for field_name, value in ( + ("priority", priority), + ("utility", utility), + ("risk_limit", risk_limit), + ): + if not 0.0 <= value <= 1.0: + raise FoundationError(f"goal {field_name} must be between 0 and 1") + evidence = tuple(evidence_digests) + for digest in evidence: + digest.require_algorithm("sha256") + reason = require_optional_text(status_reason, field_name="status_reason") + if status in {GoalStatus.BLOCKED, GoalStatus.ABANDONED} and reason is None: + raise FoundationError("blocked or abandoned goals require a status reason") + dependencies = tuple( + sorted( + { + CanonicalKey.from_text(item, field_name="dependency_id") + for item in dependency_ids + }, + key=lambda item: item.value, + ) + ) + canonical_id = CanonicalKey.from_text(goal_id, field_name="goal_id") + if canonical_id in dependencies: + raise FoundationError("goal must not depend on itself") + return cls( + goal_id=canonical_id, + description=require_text(description, field_name="description"), + desired_state=desired_state, + priority=priority, + utility=utility, + risk_limit=risk_limit, + status=status, + dependency_ids=dependencies, + authority_required=authority_required, + evidence_digests=evidence, + status_reason=reason, + ) + + def with_status(self, status: GoalStatus, *, reason: str | None = None) -> GoalSpec: + """Return the same goal with an explicit lifecycle transition.""" + return GoalSpec.create( + goal_id=self.goal_id.value, + description=self.description, + desired_state=self.desired_state, + priority=self.priority, + utility=self.utility, + risk_limit=self.risk_limit, + status=status, + dependency_ids=(item.value for item in self.dependency_ids), + authority_required=self.authority_required, + evidence_digests=self.evidence_digests, + status_reason=reason, + ) + + def to_payload(self) -> JsonObject: + """Return a canonical goal payload.""" + evidence: JsonArray = [ + {"algorithm": item.algorithm, "value": item.value} for item in self.evidence_digests + ] + dependencies: JsonArray = [item.value for item in self.dependency_ids] + return { + "goal_id": self.goal_id.value, + "description": self.description, + "desired_state": self.desired_state.to_payload(), + "priority": self.priority, + "utility": self.utility, + "risk_limit": self.risk_limit, + "status": self.status.value, + "dependency_ids": dependencies, + "authority_required": self.authority_required, + "evidence_digests": evidence, + "status_reason": self.status_reason, + } + + def digest(self) -> DigestRecord: + """Return a deterministic identity for this goal state.""" + return DigestRecord.from_payload(self.to_payload()) + + +@dataclass(frozen=True, slots=True) +class GoalGraph: + """Immutable dependency graph for bounded cognitive goals.""" + + goals: tuple[GoalSpec, ...] = () + + @classmethod + def create(cls, goals: Iterable[GoalSpec] = ()) -> GoalGraph: + """Create a graph and reject missing dependencies or dependency cycles.""" + normalized = tuple(sorted(goals, key=lambda item: item.goal_id.value)) + identifiers = [item.goal_id.value for item in normalized] + if len(identifiers) != len(set(identifiers)): + raise FoundationError("goal graph contains duplicate identifiers") + known = set(identifiers) + for goal in normalized: + missing = sorted( + dependency.value + for dependency in goal.dependency_ids + if dependency.value not in known + ) + if missing: + raise FoundationError( + f"goal {goal.goal_id.value} has unknown dependencies: {', '.join(missing)}" + ) + graph = cls(normalized) + graph._require_acyclic() + return graph + + def _require_acyclic(self) -> None: + """Reject dependency cycles with deterministic diagnostics.""" + by_id = {goal.goal_id.value: goal for goal in self.goals} + visiting: set[str] = set() + visited: set[str] = set() + + def visit(goal_id: str, trail: tuple[str, ...]) -> None: + if goal_id in visited: + return + if goal_id in visiting: + cycle = " -> ".join((*trail, goal_id)) + raise FoundationError(f"goal dependency cycle detected: {cycle}") + visiting.add(goal_id) + goal = by_id[goal_id] + for dependency in goal.dependency_ids: + visit(dependency.value, (*trail, goal_id)) + visiting.remove(goal_id) + visited.add(goal_id) + + for goal_id in sorted(by_id): + visit(goal_id, ()) + + def require(self, goal_id: str) -> GoalSpec: + """Return one goal by canonical identifier.""" + requested = CanonicalKey.from_text(goal_id, field_name="goal_id") + for goal in self.goals: + if goal.goal_id == requested: + return goal + raise FoundationError(f"unknown goal id: {requested.value}") + + def add(self, goal: GoalSpec) -> GoalGraph: + """Return a graph with one unique goal added.""" + return GoalGraph.create((*self.goals, goal)) + + def update_status( + self, + goal_id: str, + status: GoalStatus, + *, + reason: str | None = None, + ) -> GoalGraph: + """Return a graph with one explicit goal-state transition.""" + requested = self.require(goal_id) + updated = requested.with_status(status, reason=reason) + return GoalGraph.create(updated if goal == requested else goal for goal in self.goals) + + def dependencies_satisfied(self, goal: GoalSpec) -> bool: + """Return whether all dependency goals are explicitly satisfied.""" + by_id = {item.goal_id: item for item in self.goals} + return all( + by_id[dependency].status is GoalStatus.SATISFIED for dependency in goal.dependency_ids + ) + + def selectable(self, world_model: WorldModel) -> tuple[GoalSpec, ...]: + """Return goals eligible for executive selection in stable priority order.""" + state = world_model.state() + candidates = [] + for goal in self.goals: + if goal.status not in {GoalStatus.PROPOSED, GoalStatus.ACTIVE}: + continue + if goal.desired_state.matches(state): + continue + if not self.dependencies_satisfied(goal): + continue + candidates.append(goal) + return tuple( + sorted( + candidates, + key=lambda item: ( + -item.priority, + -item.utility, + item.goal_id.value, + ), + ) + ) + + def select(self, world_model: WorldModel) -> GoalSpec | None: + """Return the highest-priority currently selectable goal.""" + candidates = self.selectable(world_model) + return candidates[0] if candidates else None + + def reconcile(self, world_model: WorldModel) -> GoalGraph: + """Mark goals satisfied only when their desired state is present.""" + state = world_model.state() + reconciled = tuple( + goal.with_status(GoalStatus.SATISFIED) + if goal.status not in {GoalStatus.ABANDONED, GoalStatus.SATISFIED} + and goal.desired_state.matches(state) + else goal + for goal in self.goals + ) + return GoalGraph.create(reconciled) + + def to_payload(self) -> JsonObject: + """Return a canonical goal-graph payload.""" + goals: JsonArray = [goal.to_payload() for goal in self.goals] + return {"goals": goals} + + def digest(self) -> DigestRecord: + """Return a deterministic graph identity.""" + return DigestRecord.from_payload(self.to_payload()) diff --git a/src/ix_sally/cognition/governance_bridge.py b/src/ix_sally/cognition/governance_bridge.py new file mode 100644 index 0000000..139109e --- /dev/null +++ b/src/ix_sally/cognition/governance_bridge.py @@ -0,0 +1,155 @@ +"""Bridge cognitive plan proposals into the existing IX-Sally control plane.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from ix_sally.agents import AgentRole +from ix_sally.claims import ClaimRecord, ClaimStatus +from ix_sally.cognition.executive import ExecutiveDecision +from ix_sally.cognition.planning import ActionSpec +from ix_sally.digest import DigestRecord, JsonArray, JsonObject +from ix_sally.foundation import CanonicalKey, FoundationError +from ix_sally.proposals import ProposalAction, SallyProposalPacket + + +@dataclass(frozen=True, slots=True) +class CognitiveProposalBridgeReceipt: + """Receipt linking one executive decision to a control-plane proposal packet.""" + + bridge_id: CanonicalKey + executive_decision_digest: DigestRecord + proposal_digest: DigestRecord + action_links: tuple[tuple[CanonicalKey, CanonicalKey], ...] + + def to_payload(self) -> JsonObject: + """Return a canonical bridge receipt.""" + links: JsonArray = [ + { + "cognitive_action_id": cognitive.value, + "proposal_action_id": proposal.value, + } + for cognitive, proposal in self.action_links + ] + return { + "bridge_id": self.bridge_id.value, + "executive_decision_digest": { + "algorithm": self.executive_decision_digest.algorithm, + "value": self.executive_decision_digest.value, + }, + "proposal_digest": { + "algorithm": self.proposal_digest.algorithm, + "value": self.proposal_digest.value, + }, + "action_links": links, + } + + def digest(self) -> DigestRecord: + """Return a deterministic bridge receipt identity.""" + return DigestRecord.from_payload(self.to_payload()) + + +@dataclass(frozen=True, slots=True) +class CognitiveProposalBridgeResult: + """Control-plane proposal and its explicit cognitive provenance receipt.""" + + proposal: SallyProposalPacket + receipt: CognitiveProposalBridgeReceipt + + def to_payload(self) -> JsonObject: + """Return a canonical bridge result.""" + return { + "proposal": self.proposal.to_payload(), + "receipt": self.receipt.to_payload(), + } + + def digest(self) -> DigestRecord: + """Return a deterministic bridge-result identity.""" + return DigestRecord.from_payload(self.to_payload()) + + +@dataclass(frozen=True, slots=True) +class CognitiveProposalBridge: + """Convert approved-for-governance cognition into proposals, never execution.""" + + intended_authority: str = "cognitive-plan-execution" + + def bridge( + self, + *, + decision: ExecutiveDecision, + cycle: int, + ) -> CognitiveProposalBridgeResult: + """Convert a plan-ready decision into the existing proposal schema.""" + if cycle < 0: + raise FoundationError("proposal bridge cycle must not be negative") + selected_goal = decision.selected_goal + plan = decision.plan + if not decision.may_enter_governance() or plan is None or selected_goal is None: + raise FoundationError( + "only plan-ready executive decisions may enter proposal governance" + ) + proposal_actions = tuple(self._proposal_action(action) for action in plan.actions) + claim_status = ( + ClaimStatus.PARTIAL + if decision.status.value == "requires_human" + else ClaimStatus.SUPPORTED + ) + support = ( + decision.evidence_digests + if claim_status is ClaimStatus.SUPPORTED + else (decision.digest(),) + ) + claim = ClaimRecord.create( + cycle=cycle, + author=AgentRole.SALLY, + statement=( + "The bounded planner found a declarative action sequence for the " + f"selected goal {selected_goal.goal_id.value}." + ), + status=claim_status, + support_digests=support, + ) + proposal = SallyProposalPacket.create( + cycle=cycle, + goal_interpretation=selected_goal.description, + rationale=decision.rationale, + proposed_actions=proposal_actions, + claims=(claim,), + ) + decision_digest = decision.digest() + links = tuple( + (action.action_id, proposal_action.action_id) + for action, proposal_action in zip( + plan.actions, + proposal_actions, + strict=True, + ) + ) + bridge_seed = DigestRecord.from_payload( + { + "decision": decision_digest.value, + "proposal": proposal.digest().value, + } + ) + receipt = CognitiveProposalBridgeReceipt( + bridge_id=CanonicalKey.from_text( + f"cognitive-proposal-bridge-{bridge_seed.value[:24]}", + field_name="bridge_id", + ), + executive_decision_digest=decision_digest, + proposal_digest=proposal.digest(), + action_links=links, + ) + return CognitiveProposalBridgeResult(proposal=proposal, receipt=receipt) + + def _proposal_action(self, action: ActionSpec) -> ProposalAction: + """Convert one declarative action while preserving authority boundaries.""" + return ProposalAction.create( + action_id=action.action_id, + description=action.description, + intended_authority=self.intended_authority, + requires_tool=False, + requires_memory_write=False, + requires_human_boundary=action.authority_required, + ) diff --git a/src/ix_sally/cognition/learning.py b/src/ix_sally/cognition/learning.py new file mode 100644 index 0000000..f5c7a6e --- /dev/null +++ b/src/ix_sally/cognition/learning.py @@ -0,0 +1,280 @@ +"""Outcome-driven skill learning, retention, curriculum, and transfer evaluation.""" + +from __future__ import annotations + +from collections.abc import Iterable +from dataclasses import dataclass +from enum import StrEnum + +from ix_sally.digest import DigestRecord, JsonArray, JsonObject, JsonValue +from ix_sally.foundation import CanonicalKey, FoundationError, require_text + + +class OutcomeStatus(StrEnum): + """Observed outcome class used by the learning system.""" + + SUCCESS = "success" + PARTIAL = "partial" + FAILURE = "failure" + INVALID = "invalid" + + +@dataclass(frozen=True, slots=True) +class LearningOutcome: + """One evidence-bound result from attempting a task with a skill.""" + + outcome_id: CanonicalKey + skill_id: CanonicalKey + task_family: CanonicalKey + status: OutcomeStatus + score: float + evidence_digest: DigestRecord + notes: str + + @classmethod + def create( + cls, + *, + outcome_id: str, + skill_id: str, + task_family: str, + status: OutcomeStatus, + score: float, + evidence_digest: DigestRecord, + notes: str, + ) -> LearningOutcome: + """Create an outcome without treating an unevidenced score as learning.""" + if not 0.0 <= score <= 1.0: + raise FoundationError("learning outcome score must be between 0 and 1") + evidence_digest.require_algorithm("sha256") + return cls( + outcome_id=CanonicalKey.from_text(outcome_id, field_name="outcome_id"), + skill_id=CanonicalKey.from_text(skill_id, field_name="skill_id"), + task_family=CanonicalKey.from_text( + task_family, + field_name="task_family", + ), + status=status, + score=score, + evidence_digest=evidence_digest, + notes=require_text(notes, field_name="notes"), + ) + + def to_payload(self) -> JsonObject: + """Return a canonical learning-outcome payload.""" + return { + "outcome_id": self.outcome_id.value, + "skill_id": self.skill_id.value, + "task_family": self.task_family.value, + "status": self.status.value, + "score": self.score, + "evidence_digest": { + "algorithm": self.evidence_digest.algorithm, + "value": self.evidence_digest.value, + }, + "notes": self.notes, + } + + def digest(self) -> DigestRecord: + """Return a deterministic outcome identity.""" + return DigestRecord.from_payload(self.to_payload()) + + +@dataclass(frozen=True, slots=True) +class SkillProfile: + """Evidence-based competence estimate for one reusable skill.""" + + skill_id: CanonicalKey + attempts: int = 0 + successes: int = 0 + mean_score: float = 0.0 + confidence: float = 0.0 + last_outcome_digest: DigestRecord | None = None + + @classmethod + def new(cls, skill_id: str) -> SkillProfile: + """Create an untested skill profile.""" + return cls(CanonicalKey.from_text(skill_id, field_name="skill_id")) + + def __post_init__(self) -> None: + """Require counters and estimates to remain coherent.""" + if self.attempts < 0 or self.successes < 0 or self.successes > self.attempts: + raise FoundationError("skill profile counters are inconsistent") + if not 0.0 <= self.mean_score <= 1.0: + raise FoundationError("skill mean_score must be between 0 and 1") + if not 0.0 <= self.confidence <= 1.0: + raise FoundationError("skill confidence must be between 0 and 1") + if self.last_outcome_digest is not None: + self.last_outcome_digest.require_algorithm("sha256") + + def learn(self, outcome: LearningOutcome) -> SkillProfile: + """Return a profile updated by one outcome for the same skill.""" + if outcome.skill_id != self.skill_id: + raise FoundationError("learning outcome skill does not match profile") + attempts = self.attempts + 1 + mean_score = ((self.mean_score * self.attempts) + outcome.score) / attempts + successes = self.successes + int(outcome.status is OutcomeStatus.SUCCESS) + confidence = min(1.0, attempts / 12.0) * (0.5 + 0.5 * mean_score) + return SkillProfile( + skill_id=self.skill_id, + attempts=attempts, + successes=successes, + mean_score=round(mean_score, 12), + confidence=round(confidence, 12), + last_outcome_digest=outcome.digest(), + ) + + def to_payload(self) -> JsonObject: + """Return a canonical skill-profile payload.""" + digest_payload: JsonValue = None + if self.last_outcome_digest is not None: + digest_payload = { + "algorithm": self.last_outcome_digest.algorithm, + "value": self.last_outcome_digest.value, + } + return { + "skill_id": self.skill_id.value, + "attempts": self.attempts, + "successes": self.successes, + "mean_score": self.mean_score, + "confidence": self.confidence, + "last_outcome_digest": digest_payload, + } + + +@dataclass(frozen=True, slots=True) +class LearningLedger: + """Immutable outcomes and skill profiles with deterministic updates.""" + + outcomes: tuple[LearningOutcome, ...] = () + profiles: tuple[SkillProfile, ...] = () + + def __post_init__(self) -> None: + """Reject duplicate outcome and skill identifiers.""" + outcome_ids = [outcome.outcome_id.value for outcome in self.outcomes] + skill_ids = [profile.skill_id.value for profile in self.profiles] + if len(outcome_ids) != len(set(outcome_ids)): + raise FoundationError("learning ledger contains duplicate outcomes") + if len(skill_ids) != len(set(skill_ids)): + raise FoundationError("learning ledger contains duplicate profiles") + + def record(self, outcome: LearningOutcome) -> LearningLedger: + """Return a ledger with the outcome and updated profile.""" + if any(existing.outcome_id == outcome.outcome_id for existing in self.outcomes): + raise FoundationError(f"learning outcome already exists: {outcome.outcome_id.value}") + profiles = list(self.profiles) + for index, profile in enumerate(profiles): + if profile.skill_id == outcome.skill_id: + profiles[index] = profile.learn(outcome) + break + else: + profiles.append(SkillProfile.new(outcome.skill_id.value).learn(outcome)) + return LearningLedger( + outcomes=(*self.outcomes, outcome), + profiles=tuple(sorted(profiles, key=lambda item: item.skill_id.value)), + ) + + def require_profile(self, skill_id: str) -> SkillProfile: + """Return one skill profile by canonical identifier.""" + requested = CanonicalKey.from_text(skill_id, field_name="skill_id") + for profile in self.profiles: + if profile.skill_id == requested: + return profile + raise FoundationError(f"unknown skill profile: {requested.value}") + + def retention_score(self, skill_id: str, *, recent_window: int = 5) -> float: + """Return mean recent performance for transparent retention tracking.""" + if recent_window <= 0: + raise FoundationError("retention recent_window must be positive") + requested = CanonicalKey.from_text(skill_id, field_name="skill_id") + relevant = [outcome.score for outcome in self.outcomes if outcome.skill_id == requested][ + -recent_window: + ] + if not relevant: + return 0.0 + return round(sum(relevant) / len(relevant), 12) + + def to_payload(self) -> JsonObject: + """Return a canonical learning-ledger payload.""" + outcomes: JsonArray = [outcome.to_payload() for outcome in self.outcomes] + profiles: JsonArray = [profile.to_payload() for profile in self.profiles] + return { + "outcome_count": len(self.outcomes), + "profile_count": len(self.profiles), + "outcomes": outcomes, + "profiles": profiles, + } + + def digest(self) -> DigestRecord: + """Return a deterministic learning-ledger identity.""" + return DigestRecord.from_payload(self.to_payload()) + + +@dataclass(frozen=True, slots=True) +class TransferEvaluation: + """Measured performance across familiar and held-out task families.""" + + skill_id: CanonicalKey + familiar_score: float + novel_score: float + retention_score: float + evidence_digests: tuple[DigestRecord, ...] + + @classmethod + def create( + cls, + *, + skill_id: str, + familiar_score: float, + novel_score: float, + retention_score: float, + evidence_digests: Iterable[DigestRecord], + ) -> TransferEvaluation: + """Create a transfer result with explicit supporting evidence.""" + for name, value in { + "familiar_score": familiar_score, + "novel_score": novel_score, + "retention_score": retention_score, + }.items(): + if not 0.0 <= value <= 1.0: + raise FoundationError(f"transfer {name} must be between 0 and 1") + evidence = tuple(evidence_digests) + if not evidence: + raise FoundationError("transfer evaluation requires evidence") + for digest in evidence: + digest.require_algorithm("sha256") + return cls( + skill_id=CanonicalKey.from_text(skill_id, field_name="skill_id"), + familiar_score=familiar_score, + novel_score=novel_score, + retention_score=retention_score, + evidence_digests=evidence, + ) + + def generalization_gap(self) -> float: + """Return familiar minus novel performance.""" + return round(self.familiar_score - self.novel_score, 12) + + def passes(self, *, minimum_novel_score: float = 0.7, maximum_gap: float = 0.2) -> bool: + """Return whether the measured transfer meets declared thresholds.""" + return ( + self.novel_score >= minimum_novel_score + and self.retention_score >= minimum_novel_score + and self.generalization_gap() <= maximum_gap + ) + + def to_payload(self) -> JsonObject: + """Return a canonical transfer-evaluation payload.""" + evidence: JsonArray = [ + {"algorithm": digest.algorithm, "value": digest.value} + for digest in self.evidence_digests + ] + return { + "skill_id": self.skill_id.value, + "familiar_score": self.familiar_score, + "novel_score": self.novel_score, + "retention_score": self.retention_score, + "generalization_gap": self.generalization_gap(), + "passes": self.passes(), + "evidence_digests": evidence, + } diff --git a/src/ix_sally/cognition/metacognition.py b/src/ix_sally/cognition/metacognition.py new file mode 100644 index 0000000..c9369c3 --- /dev/null +++ b/src/ix_sally/cognition/metacognition.py @@ -0,0 +1,225 @@ +"""Bounded self-modeling and human-authorized improvement proposals.""" + +from __future__ import annotations + +from collections.abc import Iterable +from dataclasses import dataclass +from enum import StrEnum + +from ix_sally.digest import DigestRecord, JsonArray, JsonObject +from ix_sally.foundation import CanonicalKey, FoundationError, require_text + + +class ImprovementStatus(StrEnum): + """Authority state of one proposed system improvement.""" + + PROPOSED = "proposed" + HUMAN_APPROVED = "human_approved" + HUMAN_REJECTED = "human_rejected" + VALIDATED = "validated" + REVERTED = "reverted" + + +@dataclass(frozen=True, slots=True) +class CapabilityMeasure: + """One measured capability value with evidence and declared limits.""" + + capability_id: CanonicalKey + score: float + evidence_digests: tuple[DigestRecord, ...] + limitation: str + + @classmethod + def create( + cls, + *, + capability_id: str, + score: float, + evidence_digests: Iterable[DigestRecord], + limitation: str, + ) -> CapabilityMeasure: + """Create a bounded capability measure.""" + if not 0.0 <= score <= 1.0: + raise FoundationError("capability score must be between 0 and 1") + evidence = tuple(evidence_digests) + if not evidence: + raise FoundationError("capability measure requires evidence") + for digest in evidence: + digest.require_algorithm("sha256") + return cls( + capability_id=CanonicalKey.from_text( + capability_id, + field_name="capability_id", + ), + score=score, + evidence_digests=evidence, + limitation=require_text(limitation, field_name="limitation"), + ) + + def to_payload(self) -> JsonObject: + """Return a canonical capability-measure payload.""" + evidence: JsonArray = [ + {"algorithm": digest.algorithm, "value": digest.value} + for digest in self.evidence_digests + ] + return { + "capability_id": self.capability_id.value, + "score": self.score, + "evidence_digests": evidence, + "limitation": self.limitation, + } + + +@dataclass(frozen=True, slots=True) +class SelfModel: + """Evidence-bound self-description that cannot certify its own competence.""" + + measures: tuple[CapabilityMeasure, ...] = () + + def __post_init__(self) -> None: + """Reject duplicate capability identities.""" + identifiers = [measure.capability_id.value for measure in self.measures] + if len(identifiers) != len(set(identifiers)): + raise FoundationError("self model contains duplicate capability measures") + + def update(self, measure: CapabilityMeasure) -> SelfModel: + """Replace or append one measured capability.""" + retained = tuple( + existing + for existing in self.measures + if existing.capability_id != measure.capability_id + ) + return SelfModel( + tuple( + sorted( + (*retained, measure), + key=lambda item: item.capability_id.value, + ) + ) + ) + + def weakest(self) -> CapabilityMeasure | None: + """Return the weakest measured capability, if any.""" + if not self.measures: + return None + return min(self.measures, key=lambda item: (item.score, item.capability_id.value)) + + def to_payload(self) -> JsonObject: + """Return a canonical self-model payload.""" + measures: JsonArray = [measure.to_payload() for measure in self.measures] + return {"measure_count": len(self.measures), "measures": measures} + + def digest(self) -> DigestRecord: + """Return a deterministic self-model identity.""" + return DigestRecord.from_payload(self.to_payload()) + + +@dataclass(frozen=True, slots=True) +class ImprovementProposal: + """One bounded change proposal that cannot authorize or apply itself.""" + + proposal_id: CanonicalKey + target_capability: CanonicalKey + description: str + expected_benefit: float + regression_risk: float + status: ImprovementStatus + evidence_digests: tuple[DigestRecord, ...] + human_decision_digest: DigestRecord | None = None + + @classmethod + def create( + cls, + *, + proposal_id: str, + target_capability: str, + description: str, + expected_benefit: float, + regression_risk: float, + evidence_digests: Iterable[DigestRecord], + status: ImprovementStatus = ImprovementStatus.PROPOSED, + human_decision_digest: DigestRecord | None = None, + ) -> ImprovementProposal: + """Create a proposal and enforce its human authority transition.""" + for name, value in { + "expected_benefit": expected_benefit, + "regression_risk": regression_risk, + }.items(): + if not 0.0 <= value <= 1.0: + raise FoundationError(f"improvement {name} must be between 0 and 1") + evidence = tuple(evidence_digests) + if not evidence: + raise FoundationError("improvement proposal requires evidence") + for digest in evidence: + digest.require_algorithm("sha256") + if human_decision_digest is not None: + human_decision_digest.require_algorithm("sha256") + if status is not ImprovementStatus.PROPOSED and human_decision_digest is None: + raise FoundationError( + "non-proposed improvement status requires a human decision digest" + ) + return cls( + proposal_id=CanonicalKey.from_text(proposal_id, field_name="proposal_id"), + target_capability=CanonicalKey.from_text( + target_capability, + field_name="target_capability", + ), + description=require_text(description, field_name="description"), + expected_benefit=expected_benefit, + regression_risk=regression_risk, + status=status, + evidence_digests=evidence, + human_decision_digest=human_decision_digest, + ) + + def with_human_decision( + self, + *, + approved: bool, + decision_digest: DigestRecord, + ) -> ImprovementProposal: + """Return an explicitly human-approved or human-rejected proposal.""" + return ImprovementProposal.create( + proposal_id=self.proposal_id.value, + target_capability=self.target_capability.value, + description=self.description, + expected_benefit=self.expected_benefit, + regression_risk=self.regression_risk, + evidence_digests=self.evidence_digests, + status=( + ImprovementStatus.HUMAN_APPROVED if approved else ImprovementStatus.HUMAN_REJECTED + ), + human_decision_digest=decision_digest, + ) + + def may_enter_validation(self) -> bool: + """Return whether controlled validation may begin.""" + return self.status is ImprovementStatus.HUMAN_APPROVED + + def to_payload(self) -> JsonObject: + """Return a canonical improvement-proposal payload.""" + evidence: JsonArray = [ + {"algorithm": digest.algorithm, "value": digest.value} + for digest in self.evidence_digests + ] + human_decision: JsonObject | None = None + if self.human_decision_digest is not None: + human_decision = { + "algorithm": self.human_decision_digest.algorithm, + "value": self.human_decision_digest.value, + } + return { + "proposal_id": self.proposal_id.value, + "target_capability": self.target_capability.value, + "description": self.description, + "expected_benefit": self.expected_benefit, + "regression_risk": self.regression_risk, + "status": self.status.value, + "evidence_digests": evidence, + "human_decision_digest": human_decision, + "may_enter_validation": self.may_enter_validation(), + } + + def digest(self) -> DigestRecord: + """Return a deterministic proposal identity.""" + return DigestRecord.from_payload(self.to_payload()) diff --git a/src/ix_sally/cognition/ninefold.py b/src/ix_sally/cognition/ninefold.py new file mode 100644 index 0000000..016caa4 --- /dev/null +++ b/src/ix_sally/cognition/ninefold.py @@ -0,0 +1,249 @@ +"""Functional ninefold cognition without theatrical agent conversation.""" + +from __future__ import annotations + +from collections.abc import Iterable +from dataclasses import dataclass, field + +from ix_sally.agents import AgentRole, default_agent_role_registry +from ix_sally.cognition.active_memory import ActiveMemoryStore +from ix_sally.cognition.learning import LearningLedger +from ix_sally.cognition.planning import ActionSpec, DeterministicPlanner, Plan, PlanStatus +from ix_sally.cognition.workspace import CognitiveWorkspace, WorkspaceItemKind +from ix_sally.cognition.world_model import FactPattern, FactStatus, WorldModel +from ix_sally.digest import DigestRecord, JsonArray, JsonObject +from ix_sally.foundation import CanonicalKey, FoundationError, require_text + + +@dataclass(frozen=True, slots=True) +class RoleFinding: + """One inspectable output from a ninefold cognitive function.""" + + role: AgentRole + summary: str + detail: str + blocking: bool = False + evidence_digests: tuple[DigestRecord, ...] = () + + @classmethod + def create( + cls, + *, + role: AgentRole, + summary: str, + detail: str, + blocking: bool = False, + evidence_digests: Iterable[DigestRecord] = (), + ) -> RoleFinding: + """Create a normalized role finding.""" + evidence = tuple(evidence_digests) + for digest in evidence: + digest.require_algorithm("sha256") + return cls( + role=role, + summary=require_text(summary, field_name="summary"), + detail=require_text(detail, field_name="detail"), + blocking=blocking, + evidence_digests=evidence, + ) + + def to_payload(self) -> JsonObject: + """Return a canonical role-finding payload.""" + evidence: JsonArray = [ + {"algorithm": digest.algorithm, "value": digest.value} + for digest in self.evidence_digests + ] + return { + "role": self.role.value, + "summary": self.summary, + "detail": self.detail, + "blocking": self.blocking, + "evidence_digests": evidence, + } + + +@dataclass(frozen=True, slots=True) +class NinefoldCognitiveCycle: + """One coordinated cycle across all nine non-overlapping cognitive functions.""" + + cycle_id: CanonicalKey + task: str + findings: tuple[RoleFinding, ...] + proposed_plan: Plan | None + + def __post_init__(self) -> None: + """Require complete exactly-once coverage of the nine canonical roles.""" + roles = [finding.role for finding in self.findings] + if len(roles) != len(AgentRole) or set(roles) != set(AgentRole): + raise FoundationError("ninefold cycle requires exactly one finding per role") + + def blocking_findings(self) -> tuple[RoleFinding, ...]: + """Return findings that block action or unsupported conclusions.""" + return tuple(finding for finding in self.findings if finding.blocking) + + def to_payload(self) -> JsonObject: + """Return a canonical cycle payload.""" + findings: JsonArray = [finding.to_payload() for finding in self.findings] + plan_payload: JsonObject | None = None + if self.proposed_plan is not None: + plan_payload = self.proposed_plan.to_payload() + return { + "cycle_id": self.cycle_id.value, + "task": self.task, + "findings": findings, + "blocking_count": len(self.blocking_findings()), + "proposed_plan": plan_payload, + } + + def digest(self) -> DigestRecord: + """Return a deterministic cycle identity.""" + return DigestRecord.from_payload(self.to_payload()) + + +@dataclass(frozen=True, slots=True) +class NinefoldCoordinator: + """Coordinate functional roles from shared state instead of simulated personalities.""" + + planner: DeterministicPlanner = field(default_factory=DeterministicPlanner) + + def run( + self, + *, + task: str, + workspace: CognitiveWorkspace, + memory: ActiveMemoryStore, + world_model: WorldModel, + learning: LearningLedger, + actions: Iterable[ActionSpec] = (), + goal: FactPattern | None = None, + ) -> NinefoldCognitiveCycle: + """Run one deterministic, receipt-producing ninefold cognitive cycle.""" + normalized_task = require_text(task, field_name="task") + action_catalog = tuple(actions) + registry = default_agent_role_registry() + registry.require_complete_ninefold() + focus = workspace.focus() + retrievals = memory.retrieve(normalized_task, limit=3) + predictions = world_model.predict() + plan = None + if goal is not None: + plan = self.planner.plan( + world_model=world_model, + actions=action_catalog, + goal=goal, + ) + + risks = tuple(item for item in workspace.items if item.kind is WorkspaceItemKind.RISK) + hypotheses = tuple( + item for item in workspace.items if item.kind is WorkspaceItemKind.HYPOTHESIS + ) + unsupported_hypotheses = tuple(item for item in hypotheses if not item.evidence_digests) + observed_count = sum(1 for fact in world_model.facts if fact.status is FactStatus.OBSERVED) + verified_memory_count = sum(1 for entry in memory.entries if entry.is_retrievable_truth()) + authority_block = plan is not None and plan.requires_human_authority() + findings = ( + RoleFinding.create( + role=AgentRole.SALLY, + summary="Constructed current task proposal context.", + detail=( + f"Task has {len(focus)} focused workspace items and " + f"{len(action_catalog)} declared planning actions." + ), + evidence_digests=(workspace.digest(),), + ), + RoleFinding.create( + role=AgentRole.BUTCH, + summary="Tested assumptions for unsupported hypotheses.", + detail=( + f"Found {len(unsupported_hypotheses)} hypotheses without " + "explicit evidence bindings." + ), + blocking=bool(unsupported_hypotheses), + evidence_digests=(workspace.digest(),), + ), + RoleFinding.create( + role=AgentRole.VERITY, + summary="Measured evidence-backed state.", + detail=( + f"World model contains {observed_count} observed facts and memory " + f"contains {verified_memory_count} verified entries." + ), + evidence_digests=(world_model.digest(), memory.digest()), + ), + RoleFinding.create( + role=AgentRole.ORACLE, + summary="Produced causal predictions before action.", + detail=f"Generated {len(predictions)} evidence-bound predictions.", + evidence_digests=(world_model.digest(),), + ), + RoleFinding.create( + role=AgentRole.FORGE, + summary="Evaluated the bounded execution path.", + detail=( + "No goal was supplied for planning." + if plan is None + else f"Planner result: {plan.status.value}." + ), + blocking=( + plan is not None and not plan.actions and plan.status is PlanStatus.NOT_FOUND + ), + evidence_digests=(plan.digest(),) if plan is not None else (), + ), + RoleFinding.create( + role=AgentRole.MNEMOSYNE, + summary="Retrieved relevant memory without promoting candidates to truth.", + detail=f"Retrieved {len(retrievals)} scored memory entries.", + evidence_digests=(memory.digest(),), + ), + RoleFinding.create( + role=AgentRole.SENTINEL, + summary="Checked risk and human authority boundaries.", + detail=( + f"Workspace has {len(risks)} explicit risks; " + f"human authority required: {authority_block}." + ), + blocking=authority_block, + evidence_digests=(workspace.digest(),) + ((plan.digest(),) if plan else ()), + ), + RoleFinding.create( + role=AgentRole.TRANSFER, + summary="Measured available learning and retention evidence.", + detail=( + f"Learning ledger contains {len(learning.profiles)} skill profiles " + f"from {len(learning.outcomes)} outcomes." + ), + evidence_digests=(learning.digest(),), + ), + RoleFinding.create( + role=AgentRole.CLERK, + summary="Recorded the complete deterministic cycle state.", + detail="All role findings are digest-bound and ordered by canonical role.", + evidence_digests=( + workspace.digest(), + memory.digest(), + world_model.digest(), + learning.digest(), + ), + ), + ) + ordered = tuple( + next(finding for finding in findings if finding.role is role) for role in AgentRole + ) + cycle_seed = DigestRecord.from_payload( + { + "task": normalized_task, + "workspace": workspace.digest().value, + "memory": memory.digest().value, + "world": world_model.digest().value, + "learning": learning.digest().value, + } + ) + return NinefoldCognitiveCycle( + cycle_id=CanonicalKey.from_text( + f"cognitive-cycle-{cycle_seed.value[:20]}", + field_name="cycle_id", + ), + task=normalized_task, + findings=ordered, + proposed_plan=plan, + ) diff --git a/src/ix_sally/cognition/persistence.py b/src/ix_sally/cognition/persistence.py new file mode 100644 index 0000000..6417e51 --- /dev/null +++ b/src/ix_sally/cognition/persistence.py @@ -0,0 +1,97 @@ +"""Tamper-evident canonical persistence envelopes for IX-Sally cognitive state.""" + +from __future__ import annotations + +import json +from dataclasses import dataclass + +from ix_sally.digest import DigestRecord, JsonObject, JsonValue, stable_json +from ix_sally.foundation import FoundationError, require_text + +CURRENT_SNAPSHOT_SCHEMA = 1 + + +@dataclass(frozen=True, slots=True) +class CognitiveSnapshot: + """Versioned canonical state payload with an independently verified digest.""" + + schema_version: int + repository: str + state: JsonObject + state_digest: DigestRecord + + @classmethod + def create( + cls, + state: JsonObject, + *, + repository: str = "IX-Sally", + ) -> CognitiveSnapshot: + """Create a current-schema snapshot for exact state data.""" + return cls( + schema_version=CURRENT_SNAPSHOT_SCHEMA, + repository=require_text(repository, field_name="repository"), + state=state, + state_digest=DigestRecord.from_payload(state), + ) + + def __post_init__(self) -> None: + """Reject unsupported schemas, wrong repository identity, and tampering.""" + if self.schema_version != CURRENT_SNAPSHOT_SCHEMA: + raise FoundationError(f"unsupported cognitive snapshot schema: {self.schema_version}") + if self.repository != "IX-Sally": + raise FoundationError(f"cognitive snapshot repository mismatch: {self.repository}") + self.state_digest.require_algorithm("sha256") + actual = DigestRecord.from_payload(self.state) + if actual != self.state_digest: + raise FoundationError("cognitive snapshot state digest mismatch") + + def to_payload(self) -> JsonObject: + """Return the complete canonical envelope payload.""" + return { + "schema_version": self.schema_version, + "repository": self.repository, + "state": self.state, + "state_digest": { + "algorithm": self.state_digest.algorithm, + "value": self.state_digest.value, + }, + } + + def to_json(self) -> str: + """Return deterministic compact JSON suitable for exact storage.""" + return stable_json(self.to_payload()) + + @classmethod + def from_json(cls, encoded: str) -> CognitiveSnapshot: + """Parse and fully validate one persisted snapshot envelope.""" + if not isinstance(encoded, str) or not encoded.strip(): + raise FoundationError("encoded snapshot must be non-empty text") + try: + payload: JsonValue = json.loads(encoded) + except json.JSONDecodeError as exc: + raise FoundationError("cognitive snapshot is not valid JSON") from exc + if not isinstance(payload, dict): + raise FoundationError("cognitive snapshot root must be an object") + schema_version = payload.get("schema_version") + repository = payload.get("repository") + state = payload.get("state") + digest_payload = payload.get("state_digest") + if not isinstance(schema_version, int) or isinstance(schema_version, bool): + raise FoundationError("snapshot schema_version must be an integer") + if not isinstance(repository, str): + raise FoundationError("snapshot repository must be text") + if not isinstance(state, dict): + raise FoundationError("snapshot state must be an object") + if not isinstance(digest_payload, dict): + raise FoundationError("snapshot state_digest must be an object") + algorithm = digest_payload.get("algorithm") + value = digest_payload.get("value") + if not isinstance(algorithm, str) or not isinstance(value, str): + raise FoundationError("snapshot state_digest fields must be text") + return cls( + schema_version=schema_version, + repository=repository, + state=state, + state_digest=DigestRecord(algorithm=algorithm, value=value), + ) diff --git a/src/ix_sally/cognition/planning.py b/src/ix_sally/cognition/planning.py new file mode 100644 index 0000000..7595645 --- /dev/null +++ b/src/ix_sally/cognition/planning.py @@ -0,0 +1,414 @@ +"""Deterministic goal planning, authority gating, and simulated execution.""" + +from __future__ import annotations + +from collections import deque +from collections.abc import Iterable, Mapping +from dataclasses import dataclass +from enum import StrEnum + +from ix_sally.cognition.values import CognitiveValue +from ix_sally.cognition.world_model import FactPattern, FactStatus, WorldFact, WorldModel +from ix_sally.digest import DigestRecord, JsonArray, JsonObject +from ix_sally.foundation import CanonicalKey, FoundationError, require_text + +StateKey = tuple[str, str] +StateMap = dict[StateKey, CognitiveValue] + + +class PlanStatus(StrEnum): + """Outcome of bounded deterministic planning.""" + + FOUND = "found" + ALREADY_SATISFIED = "already_satisfied" + NOT_FOUND = "not_found" + SEARCH_LIMIT = "search_limit" + + +class ExecutionPermission(StrEnum): + """Authority result for a simulated plan step.""" + + ALLOWED = "allowed" + REQUIRES_HUMAN = "requires_human" + DENIED = "denied" + + +@dataclass(frozen=True, slots=True) +class FactEffect: + """One exact state change produced by a planning action.""" + + subject: CanonicalKey + predicate: CanonicalKey + value: CognitiveValue + + @classmethod + def create( + cls, + *, + subject: str, + predicate: str, + value: CognitiveValue, + ) -> FactEffect: + """Create one canonical state effect.""" + return cls( + subject=CanonicalKey.from_text(subject, field_name="subject"), + predicate=CanonicalKey.from_text(predicate, field_name="predicate"), + value=value, + ) + + def key(self) -> StateKey: + """Return the affected state key.""" + return (self.subject.value, self.predicate.value) + + def to_payload(self) -> JsonObject: + """Return a canonical effect payload.""" + return { + "subject": self.subject.value, + "predicate": self.predicate.value, + "value": self.value.to_payload(), + } + + +@dataclass(frozen=True, slots=True) +class ActionSpec: + """One declarative action with exact preconditions, effects, cost, and risk.""" + + action_id: CanonicalKey + description: str + preconditions: tuple[FactPattern, ...] + effects: tuple[FactEffect, ...] + cost: float + risk: float + authority_required: bool = False + + @classmethod + def create( + cls, + *, + action_id: str, + description: str, + preconditions: Iterable[FactPattern], + effects: Iterable[FactEffect], + cost: float, + risk: float, + authority_required: bool = False, + ) -> ActionSpec: + """Create a planning action without executable side effects.""" + normalized_effects = tuple(effects) + if not normalized_effects: + raise FoundationError("planning action requires at least one effect") + if cost < 0: + raise FoundationError("planning action cost must not be negative") + if not 0.0 <= risk <= 1.0: + raise FoundationError("planning action risk must be between 0 and 1") + return cls( + action_id=CanonicalKey.from_text(action_id, field_name="action_id"), + description=require_text(description, field_name="description"), + preconditions=tuple(preconditions), + effects=normalized_effects, + cost=cost, + risk=risk, + authority_required=authority_required, + ) + + def applicable(self, state: Mapping[StateKey, CognitiveValue]) -> bool: + """Return whether all exact preconditions hold in a plain planning state.""" + return all( + state.get((condition.subject.value, condition.predicate.value)) == condition.value + for condition in self.preconditions + ) + + def apply(self, state: Mapping[StateKey, CognitiveValue]) -> StateMap: + """Return a copied state with all declared effects applied.""" + if not self.applicable(state): + raise FoundationError(f"action preconditions are not satisfied: {self.action_id.value}") + updated = dict(state) + for effect in self.effects: + updated[effect.key()] = effect.value + return updated + + def to_payload(self) -> JsonObject: + """Return a canonical action payload.""" + preconditions: JsonArray = [condition.to_payload() for condition in self.preconditions] + effects: JsonArray = [effect.to_payload() for effect in self.effects] + return { + "action_id": self.action_id.value, + "description": self.description, + "preconditions": preconditions, + "effects": effects, + "cost": self.cost, + "risk": self.risk, + "authority_required": self.authority_required, + } + + def digest(self) -> DigestRecord: + """Return a deterministic action identity.""" + return DigestRecord.from_payload(self.to_payload()) + + +@dataclass(frozen=True, slots=True) +class Plan: + """One bounded plan and its complete search result.""" + + status: PlanStatus + goal: FactPattern + actions: tuple[ActionSpec, ...] + explored_states: int + total_cost: float + aggregate_risk: float + reason: str + + def __post_init__(self) -> None: + """Require plan metrics and status to remain coherent.""" + if self.explored_states < 0: + raise FoundationError("plan explored_states must not be negative") + if self.total_cost < 0: + raise FoundationError("plan total_cost must not be negative") + if not 0.0 <= self.aggregate_risk <= 1.0: + raise FoundationError("plan aggregate_risk must be between 0 and 1") + require_text(self.reason, field_name="reason") + if self.status is PlanStatus.FOUND and not self.actions: + raise FoundationError("found plan requires actions") + if self.status is not PlanStatus.FOUND and self.actions: + raise FoundationError("non-found plan must not contain actions") + + def requires_human_authority(self) -> bool: + """Return whether any action crosses a human authority boundary.""" + return any(action.authority_required for action in self.actions) + + def to_payload(self) -> JsonObject: + """Return a canonical plan receipt.""" + actions: JsonArray = [action.to_payload() for action in self.actions] + return { + "status": self.status.value, + "goal": self.goal.to_payload(), + "actions": actions, + "explored_states": self.explored_states, + "total_cost": self.total_cost, + "aggregate_risk": self.aggregate_risk, + "requires_human_authority": self.requires_human_authority(), + "reason": self.reason, + } + + def digest(self) -> DigestRecord: + """Return a deterministic plan identity.""" + return DigestRecord.from_payload(self.to_payload()) + + +@dataclass(frozen=True, slots=True) +class DeterministicPlanner: + """Breadth-first planner with stable ordering and an explicit search bound.""" + + max_explored_states: int = 5_000 + max_depth: int = 24 + + def __post_init__(self) -> None: + """Require positive planning bounds.""" + if self.max_explored_states <= 0 or self.max_depth <= 0: + raise FoundationError("planner bounds must be positive") + + def plan( + self, + *, + world_model: WorldModel, + actions: Iterable[ActionSpec], + goal: FactPattern, + ) -> Plan: + """Search for the shortest action sequence satisfying an exact goal.""" + action_catalog = tuple(sorted(actions, key=lambda action: action.action_id.value)) + identifiers = [action.action_id.value for action in action_catalog] + if len(identifiers) != len(set(identifiers)): + raise FoundationError("planner action catalog contains duplicate identifiers") + initial = { + key: fact.value + for key, fact in world_model.state().items() + if fact.status is not FactStatus.CONTRADICTED + } + if self._goal_satisfied(initial, goal): + return Plan( + status=PlanStatus.ALREADY_SATISFIED, + goal=goal, + actions=(), + explored_states=0, + total_cost=0.0, + aggregate_risk=0.0, + reason="Goal is already satisfied by the current world model.", + ) + queue: deque[tuple[StateMap, tuple[ActionSpec, ...]]] = deque([(initial, ())]) + visited = {self._state_key(initial)} + explored = 0 + while queue: + state, path = queue.popleft() + explored += 1 + if explored > self.max_explored_states: + return Plan( + status=PlanStatus.SEARCH_LIMIT, + goal=goal, + actions=(), + explored_states=explored - 1, + total_cost=0.0, + aggregate_risk=0.0, + reason="Planner reached the configured state-search limit.", + ) + if len(path) >= self.max_depth: + continue + for action in action_catalog: + if not action.applicable(state): + continue + next_state = action.apply(state) + key = self._state_key(next_state) + if key in visited: + continue + visited.add(key) + next_path = (*path, action) + if self._goal_satisfied(next_state, goal): + return self._found_plan( + goal=goal, + path=next_path, + explored=explored, + ) + queue.append((next_state, next_path)) + return Plan( + status=PlanStatus.NOT_FOUND, + goal=goal, + actions=(), + explored_states=explored, + total_cost=0.0, + aggregate_risk=0.0, + reason="No plan was found within the declared action model and bounds.", + ) + + def _found_plan( + self, + *, + goal: FactPattern, + path: tuple[ActionSpec, ...], + explored: int, + ) -> Plan: + """Create a stable found-plan receipt.""" + total_cost = round(sum(action.cost for action in path), 12) + survival = 1.0 + for action in path: + survival *= 1.0 - action.risk + aggregate_risk = round(1.0 - survival, 12) + return Plan( + status=PlanStatus.FOUND, + goal=goal, + actions=path, + explored_states=explored, + total_cost=total_cost, + aggregate_risk=aggregate_risk, + reason="A shortest deterministic plan was found.", + ) + + def _goal_satisfied( + self, + state: Mapping[StateKey, CognitiveValue], + goal: FactPattern, + ) -> bool: + """Return whether the plain state exactly satisfies the goal.""" + return state.get((goal.subject.value, goal.predicate.value)) == goal.value + + def _state_key( + self, + state: Mapping[StateKey, CognitiveValue], + ) -> tuple[tuple[str, str, str, object], ...]: + """Return a stable hashable identity for one planning state.""" + return tuple( + sorted( + ( + subject, + predicate, + value.value_type.value, + value.value, + ) + for (subject, predicate), value in state.items() + ) + ) + + +@dataclass(frozen=True, slots=True) +class PlanExecutionReceipt: + """Receipt from a simulated, authority-aware plan execution.""" + + plan_digest: DigestRecord + permission: ExecutionPermission + applied_action_ids: tuple[CanonicalKey, ...] + resulting_model: WorldModel + reason: str + + def to_payload(self) -> JsonObject: + """Return a canonical execution receipt.""" + actions: JsonArray = [action_id.value for action_id in self.applied_action_ids] + return { + "plan_digest": { + "algorithm": self.plan_digest.algorithm, + "value": self.plan_digest.value, + }, + "permission": self.permission.value, + "applied_action_ids": actions, + "resulting_model_digest": { + "algorithm": self.resulting_model.digest().algorithm, + "value": self.resulting_model.digest().value, + }, + "reason": self.reason, + } + + +@dataclass(frozen=True, slots=True) +class PlanSimulator: + """Apply plan effects only to a copied world model, never to the outside world.""" + + def execute( + self, + plan: Plan, + *, + world_model: WorldModel, + human_approved: bool = False, + ) -> PlanExecutionReceipt: + """Simulate a found plan with explicit human-boundary enforcement.""" + if plan.status is not PlanStatus.FOUND: + return PlanExecutionReceipt( + plan_digest=plan.digest(), + permission=ExecutionPermission.DENIED, + applied_action_ids=(), + resulting_model=world_model, + reason="Only a found plan may be simulated.", + ) + if plan.requires_human_authority() and not human_approved: + return PlanExecutionReceipt( + plan_digest=plan.digest(), + permission=ExecutionPermission.REQUIRES_HUMAN, + applied_action_ids=(), + resulting_model=world_model, + reason="Plan crosses a declared human authority boundary.", + ) + state = { + key: fact.value + for key, fact in world_model.state().items() + if fact.status is not FactStatus.CONTRADICTED + } + model = world_model + applied: list[CanonicalKey] = [] + for index, action in enumerate(plan.actions): + state = action.apply(state) + for effect_index, effect in enumerate(action.effects): + model = model.observe( + WorldFact.create( + fact_id=(f"simulated-{plan.digest().value[:12]}-{index}-{effect_index}"), + subject=effect.subject.value, + predicate=effect.predicate.value, + value=effect.value, + status=FactStatus.HYPOTHETICAL, + confidence=1.0 - action.risk, + evidence_digests=(action.digest(),), + ) + ) + applied.append(action.action_id) + return PlanExecutionReceipt( + plan_digest=plan.digest(), + permission=ExecutionPermission.ALLOWED, + applied_action_ids=tuple(applied), + resulting_model=model, + reason="Plan effects were applied to an isolated simulated world model.", + ) diff --git a/src/ix_sally/cognition/primitives.py b/src/ix_sally/cognition/primitives.py new file mode 100644 index 0000000..a20e45e --- /dev/null +++ b/src/ix_sally/cognition/primitives.py @@ -0,0 +1,302 @@ +"""Grounded cognitive primitive lifecycle and deterministic execution.""" + +from __future__ import annotations + +from collections.abc import Iterable +from dataclasses import dataclass +from enum import StrEnum + +from ix_sally.cognition.values import CognitiveValue +from ix_sally.digest import DigestRecord, JsonArray, JsonObject +from ix_sally.foundation import CanonicalKey, FoundationError, require_text + + +class PrimitiveKind(StrEnum): + """Closed classes of cognitive operations available to IX-Sally.""" + + TRANSFORM = "transform" + COMPARE = "compare" + SELECT = "select" + AGGREGATE = "aggregate" + + +class PrimitiveStatus(StrEnum): + """Lifecycle state for a cognitive primitive.""" + + CANDIDATE = "candidate" + GROUNDED = "grounded" + VALIDATED = "validated" + QUARANTINED = "quarantined" + RETIRED = "retired" + + +class PrimitiveOperation(StrEnum): + """Dependency-free operations implemented by the primitive executor.""" + + IDENTITY = "identity" + ADD = "add" + SUBTRACT = "subtract" + MULTIPLY = "multiply" + EQUAL = "equal" + GREATER = "greater" + LESS = "less" + ALL = "all" + ANY = "any" + FIRST = "first" + MAXIMUM = "maximum" + MINIMUM = "minimum" + + +@dataclass(frozen=True, slots=True) +class PrimitiveSpec: + """One immutable primitive with explicit grounding and validation evidence.""" + + primitive_id: CanonicalKey + kind: PrimitiveKind + operation: PrimitiveOperation + arity: int + status: PrimitiveStatus + description: str + grounding_digests: tuple[DigestRecord, ...] = () + validation_digests: tuple[DigestRecord, ...] = () + reason: str | None = None + + @classmethod + def create( + cls, + *, + primitive_id: str, + kind: PrimitiveKind, + operation: PrimitiveOperation, + arity: int, + status: PrimitiveStatus, + description: str, + grounding_digests: Iterable[DigestRecord] = (), + validation_digests: Iterable[DigestRecord] = (), + reason: str | None = None, + ) -> PrimitiveSpec: + """Create and validate a primitive lifecycle record.""" + if arity <= 0: + raise FoundationError("primitive arity must be positive") + grounding = tuple(grounding_digests) + validation = tuple(validation_digests) + for digest in (*grounding, *validation): + digest.require_algorithm("sha256") + if status in {PrimitiveStatus.GROUNDED, PrimitiveStatus.VALIDATED} and not grounding: + raise FoundationError("grounded primitive requires grounding evidence") + if status is PrimitiveStatus.VALIDATED and not validation: + raise FoundationError("validated primitive requires validation evidence") + if status in {PrimitiveStatus.QUARANTINED, PrimitiveStatus.RETIRED} and not reason: + raise FoundationError("inactive primitive requires a reason") + return cls( + primitive_id=CanonicalKey.from_text( + primitive_id, + field_name="primitive_id", + ), + kind=kind, + operation=operation, + arity=arity, + status=status, + description=require_text(description, field_name="description"), + grounding_digests=grounding, + validation_digests=validation, + reason=require_text(reason, field_name="reason") if reason else None, + ) + + def is_executable(self) -> bool: + """Return whether this primitive may run in the cognitive core.""" + return self.status is PrimitiveStatus.VALIDATED + + def to_payload(self) -> JsonObject: + """Return a deterministic primitive representation.""" + grounding: JsonArray = [ + {"algorithm": digest.algorithm, "value": digest.value} + for digest in self.grounding_digests + ] + validation: JsonArray = [ + {"algorithm": digest.algorithm, "value": digest.value} + for digest in self.validation_digests + ] + return { + "primitive_id": self.primitive_id.value, + "kind": self.kind.value, + "operation": self.operation.value, + "arity": self.arity, + "status": self.status.value, + "description": self.description, + "grounding_digests": grounding, + "validation_digests": validation, + "reason": self.reason, + } + + def digest(self) -> DigestRecord: + """Return a deterministic primitive identity.""" + return DigestRecord.from_payload(self.to_payload()) + + +@dataclass(frozen=True, slots=True) +class PrimitiveRegistry: + """Immutable registry that rejects duplicate primitive identifiers.""" + + primitives: tuple[PrimitiveSpec, ...] + + @classmethod + def create(cls, primitives: Iterable[PrimitiveSpec]) -> PrimitiveRegistry: + """Create a stable registry ordered by primitive identifier.""" + normalized = tuple(sorted(primitives, key=lambda item: item.primitive_id.value)) + identifiers = [item.primitive_id.value for item in normalized] + if len(identifiers) != len(set(identifiers)): + raise FoundationError("primitive registry contains duplicate identifiers") + return cls(normalized) + + def require(self, primitive_id: str) -> PrimitiveSpec: + """Return a primitive by canonical identifier.""" + requested = CanonicalKey.from_text(primitive_id, field_name="primitive_id") + for primitive in self.primitives: + if primitive.primitive_id == requested: + return primitive + raise FoundationError(f"unknown primitive: {requested.value}") + + def executable(self) -> tuple[PrimitiveSpec, ...]: + """Return validated primitives only.""" + return tuple(item for item in self.primitives if item.is_executable()) + + def to_payload(self) -> JsonObject: + """Return a canonical registry payload.""" + payload: JsonArray = [item.to_payload() for item in self.primitives] + return {"count": len(self.primitives), "primitives": payload} + + def digest(self) -> DigestRecord: + """Return a deterministic registry identity.""" + return DigestRecord.from_payload(self.to_payload()) + + +@dataclass(frozen=True, slots=True) +class PrimitiveExecution: + """Receipt from one validated primitive invocation.""" + + primitive_digest: DigestRecord + inputs: tuple[CognitiveValue, ...] + output: CognitiveValue + + def to_payload(self) -> JsonObject: + """Return a deterministic execution receipt.""" + inputs: JsonArray = [value.to_payload() for value in self.inputs] + return { + "primitive_digest": { + "algorithm": self.primitive_digest.algorithm, + "value": self.primitive_digest.value, + }, + "inputs": inputs, + "output": self.output.to_payload(), + } + + def digest(self) -> DigestRecord: + """Return a deterministic execution identity.""" + return DigestRecord.from_payload(self.to_payload()) + + +@dataclass(frozen=True, slots=True) +class PrimitiveExecutor: + """Execute only enumerated validated primitives without dynamic callbacks.""" + + registry: PrimitiveRegistry + + def execute( + self, + primitive_id: str, + inputs: Iterable[CognitiveValue], + ) -> PrimitiveExecution: + """Execute one primitive and return an inspectable receipt.""" + primitive = self.registry.require(primitive_id) + if not primitive.is_executable(): + raise FoundationError( + f"primitive is not validated for execution: {primitive.primitive_id.value}" + ) + normalized = tuple(inputs) + if len(normalized) != primitive.arity: + raise FoundationError( + f"primitive {primitive.primitive_id.value} requires {primitive.arity} " + f"inputs, got {len(normalized)}" + ) + output = self._apply(primitive.operation, normalized) + return PrimitiveExecution( + primitive_digest=primitive.digest(), + inputs=normalized, + output=output, + ) + + def _apply( + self, + operation: PrimitiveOperation, + inputs: tuple[CognitiveValue, ...], + ) -> CognitiveValue: + """Apply one closed primitive operation.""" + if operation is PrimitiveOperation.IDENTITY: + return inputs[0] + if operation is PrimitiveOperation.FIRST: + return inputs[0] + if operation in {PrimitiveOperation.ALL, PrimitiveOperation.ANY}: + booleans = tuple(value.require_boolean(operation=operation.value) for value in inputs) + boolean_result = all(booleans) if operation is PrimitiveOperation.ALL else any(booleans) + return CognitiveValue.from_python(boolean_result) + if operation is PrimitiveOperation.EQUAL: + left, right = inputs + return CognitiveValue.from_python( + left.value_type is right.value_type and left.value == right.value + ) + numeric = tuple(value.require_numeric(operation=operation.value) for value in inputs) + if operation is PrimitiveOperation.ADD: + return CognitiveValue.from_python(sum(numeric)) + if operation is PrimitiveOperation.SUBTRACT: + return CognitiveValue.from_python(numeric[0] - numeric[1]) + if operation is PrimitiveOperation.MULTIPLY: + product: int | float = 1 + for number in numeric: + product *= number + return CognitiveValue.from_python(product) + if operation is PrimitiveOperation.GREATER: + return CognitiveValue.from_python(numeric[0] > numeric[1]) + if operation is PrimitiveOperation.LESS: + return CognitiveValue.from_python(numeric[0] < numeric[1]) + if operation is PrimitiveOperation.MAXIMUM: + return CognitiveValue.from_python(max(numeric)) + if operation is PrimitiveOperation.MINIMUM: + return CognitiveValue.from_python(min(numeric)) + raise FoundationError(f"unsupported primitive operation: {operation.value}") + + +def default_primitive_registry() -> PrimitiveRegistry: + """Return a small validated kernel with explicit synthetic test evidence.""" + grounding = DigestRecord.from_payload( + {"source": "IX-Sally built-in primitive semantics", "version": 1} + ) + validation = DigestRecord.from_payload( + {"suite": "built-in deterministic primitive conformance", "version": 1} + ) + definitions: tuple[tuple[str, PrimitiveKind, PrimitiveOperation, int], ...] = ( + ("identity", PrimitiveKind.TRANSFORM, PrimitiveOperation.IDENTITY, 1), + ("add-two", PrimitiveKind.AGGREGATE, PrimitiveOperation.ADD, 2), + ("subtract", PrimitiveKind.TRANSFORM, PrimitiveOperation.SUBTRACT, 2), + ("multiply-two", PrimitiveKind.AGGREGATE, PrimitiveOperation.MULTIPLY, 2), + ("equal", PrimitiveKind.COMPARE, PrimitiveOperation.EQUAL, 2), + ("greater", PrimitiveKind.COMPARE, PrimitiveOperation.GREATER, 2), + ("less", PrimitiveKind.COMPARE, PrimitiveOperation.LESS, 2), + ("all-two", PrimitiveKind.AGGREGATE, PrimitiveOperation.ALL, 2), + ("any-two", PrimitiveKind.AGGREGATE, PrimitiveOperation.ANY, 2), + ("maximum-two", PrimitiveKind.SELECT, PrimitiveOperation.MAXIMUM, 2), + ("minimum-two", PrimitiveKind.SELECT, PrimitiveOperation.MINIMUM, 2), + ) + return PrimitiveRegistry.create( + PrimitiveSpec.create( + primitive_id=identifier, + kind=kind, + operation=operation, + arity=arity, + status=PrimitiveStatus.VALIDATED, + description=f"Built-in deterministic {operation.value} primitive.", + grounding_digests=(grounding,), + validation_digests=(validation,), + ) + for identifier, kind, operation, arity in definitions + ) diff --git a/src/ix_sally/cognition/restore.py b/src/ix_sally/cognition/restore.py new file mode 100644 index 0000000..e974e7a --- /dev/null +++ b/src/ix_sally/cognition/restore.py @@ -0,0 +1,680 @@ +"""Strict restoration of a complete IX-Sally cognitive system snapshot.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from ix_sally.cognition.active_memory import ( + ActiveMemoryEntry, + ActiveMemoryStatus, + ActiveMemoryStore, + MemoryLayer, +) +from ix_sally.cognition.curriculum import ( + Curriculum, + CurriculumLedger, + CurriculumSplit, + CurriculumTask, + CurriculumTrial, + TrialStatus, +) +from ix_sally.cognition.episodes import ( + CognitiveEpisode, + EpisodeLedger, + EpisodeStep, + EpisodeStepKind, + EpisodeStepStatus, +) +from ix_sally.cognition.goals import GoalGraph, GoalSpec, GoalStatus +from ix_sally.cognition.learning import ( + LearningLedger, + LearningOutcome, + OutcomeStatus, + SkillProfile, +) +from ix_sally.cognition.metacognition import CapabilityMeasure, SelfModel +from ix_sally.cognition.persistence import CognitiveSnapshot +from ix_sally.cognition.planning import ActionSpec, FactEffect +from ix_sally.cognition.primitives import ( + PrimitiveKind, + PrimitiveOperation, + PrimitiveRegistry, + PrimitiveSpec, + PrimitiveStatus, +) +from ix_sally.cognition.uncertainty import ( + CalibrationObservation, + UncertaintyLedger, +) +from ix_sally.cognition.values import CognitiveValue, value_from_payload +from ix_sally.cognition.workspace import ( + CognitiveWorkspace, + WorkspaceItem, + WorkspaceItemKind, + WorkspaceItemStatus, +) +from ix_sally.cognition.world_model import ( + CausalRule, + FactPattern, + FactStatus, + WorldFact, + WorldModel, +) +from ix_sally.digest import DigestRecord, JsonArray, JsonObject, JsonValue +from ix_sally.foundation import CanonicalKey, FoundationError + + +@dataclass(frozen=True, slots=True) +class RestoredCognitiveState: + """Fully validated component state reconstructed from a snapshot.""" + + workspace: CognitiveWorkspace + active_memory: ActiveMemoryStore + world_model: WorldModel + action_catalog: tuple[ActionSpec, ...] + learning: LearningLedger + self_model: SelfModel + goals: GoalGraph + uncertainty: UncertaintyLedger + episodes: EpisodeLedger + curriculum: CurriculumLedger | None + primitive_registry: PrimitiveRegistry + runtime_memories: dict[str, CognitiveValue] + execution_count: int + cycle_count: int + + +def _object(value: JsonValue, *, field: str) -> JsonObject: + if not isinstance(value, dict): + raise FoundationError(f"snapshot {field} must be an object") + return value + + +def _array(value: JsonValue, *, field: str) -> JsonArray: + if not isinstance(value, list): + raise FoundationError(f"snapshot {field} must be an array") + return value + + +def _text(value: JsonValue, *, field: str) -> str: + if not isinstance(value, str): + raise FoundationError(f"snapshot {field} must be text") + return value + + +def _integer(value: JsonValue, *, field: str) -> int: + if not isinstance(value, int) or isinstance(value, bool): + raise FoundationError(f"snapshot {field} must be an integer") + return value + + +def _number(value: JsonValue, *, field: str) -> float: + if not isinstance(value, int | float) or isinstance(value, bool): + raise FoundationError(f"snapshot {field} must be numeric") + return float(value) + + +def _boolean(value: JsonValue, *, field: str) -> bool: + if not isinstance(value, bool): + raise FoundationError(f"snapshot {field} must be Boolean") + return value + + +def _optional_text(value: JsonValue, *, field: str) -> str | None: + if value is None: + return None + return _text(value, field=field) + + +def _digest(value: JsonValue, *, field: str) -> DigestRecord: + payload = _object(value, field=field) + return DigestRecord( + algorithm=_text(payload.get("algorithm"), field=f"{field}.algorithm"), + value=_text(payload.get("value"), field=f"{field}.value"), + ) + + +def _digests(value: JsonValue, *, field: str) -> tuple[DigestRecord, ...]: + return tuple( + _digest(item, field=f"{field}[{index}]") + for index, item in enumerate(_array(value, field=field)) + ) + + +def _texts(value: JsonValue, *, field: str) -> tuple[str, ...]: + return tuple( + _text(item, field=f"{field}[{index}]") + for index, item in enumerate(_array(value, field=field)) + ) + + +def _restore_pattern(value: JsonValue, *, field: str) -> FactPattern: + payload = _object(value, field=field) + return FactPattern.create( + subject=_text(payload.get("subject"), field=f"{field}.subject"), + predicate=_text(payload.get("predicate"), field=f"{field}.predicate"), + value=value_from_payload(payload.get("value")), + ) + + +def _restore_workspace(value: JsonValue) -> CognitiveWorkspace: + payload = _object(value, field="workspace") + items = tuple( + WorkspaceItem.create( + item_id=_text(item_payload.get("item_id"), field="workspace.item_id"), + kind=WorkspaceItemKind(_text(item_payload.get("kind"), field="workspace.kind")), + content=_text(item_payload.get("content"), field="workspace.content"), + confidence=_number( + item_payload.get("confidence"), + field="workspace.confidence", + ), + salience=_number(item_payload.get("salience"), field="workspace.salience"), + status=WorkspaceItemStatus(_text(item_payload.get("status"), field="workspace.status")), + evidence_digests=_digests( + item_payload.get("evidence_digests"), + field="workspace.evidence_digests", + ), + parent_ids=_texts( + item_payload.get("parent_ids"), + field="workspace.parent_ids", + ), + ) + for item_payload in ( + _object(item, field="workspace.items[]") + for item in _array(payload.get("items"), field="workspace.items") + ) + ) + return CognitiveWorkspace( + items=items, + capacity=_integer(payload.get("capacity"), field="workspace.capacity"), + ) + + +def _restore_memory(value: JsonValue) -> ActiveMemoryStore: + payload = _object(value, field="active_memory") + entries = tuple( + ActiveMemoryEntry.create( + memory_id=_text(item.get("memory_id"), field="memory.memory_id"), + layer=MemoryLayer(_text(item.get("layer"), field="memory.layer")), + content=_text(item.get("content"), field="memory.content"), + confidence=_number(item.get("confidence"), field="memory.confidence"), + status=ActiveMemoryStatus(_text(item.get("status"), field="memory.status")), + sequence=_integer(item.get("sequence"), field="memory.sequence"), + evidence_digests=_digests( + item.get("evidence_digests"), + field="memory.evidence_digests", + ), + source_ids=_texts(item.get("source_ids"), field="memory.source_ids"), + tags=_texts(item.get("tags"), field="memory.tags"), + reason=_optional_text(item.get("reason"), field="memory.reason"), + ) + for item in ( + _object(entry, field="active_memory.entries[]") + for entry in _array(payload.get("entries"), field="active_memory.entries") + ) + ) + return ActiveMemoryStore(entries) + + +def _restore_world(value: JsonValue) -> WorldModel: + payload = _object(value, field="world_model") + facts = tuple( + WorldFact.create( + fact_id=_text(item.get("fact_id"), field="world.fact_id"), + subject=_text(item.get("subject"), field="world.subject"), + predicate=_text(item.get("predicate"), field="world.predicate"), + value=value_from_payload(item.get("value")), + status=FactStatus(_text(item.get("status"), field="world.status")), + confidence=_number(item.get("confidence"), field="world.confidence"), + evidence_digests=_digests( + item.get("evidence_digests"), + field="world.evidence_digests", + ), + derived_from=_texts( + item.get("derived_from"), + field="world.derived_from", + ), + ) + for item in ( + _object(fact, field="world_model.facts[]") + for fact in _array(payload.get("facts"), field="world_model.facts") + ) + ) + rules = tuple( + CausalRule.create( + rule_id=_text(item.get("rule_id"), field="world.rule_id"), + conditions=tuple( + _restore_pattern(condition, field="world.rule.conditions[]") + for condition in _array( + item.get("conditions"), + field="world.rule.conditions", + ) + ), + effect_subject=_text( + item.get("effect_subject"), + field="world.effect_subject", + ), + effect_predicate=_text( + item.get("effect_predicate"), + field="world.effect_predicate", + ), + effect_value=value_from_payload(item.get("effect_value")), + confidence=_number(item.get("confidence"), field="world.rule.confidence"), + evidence_digests=_digests( + item.get("evidence_digests"), + field="world.rule.evidence_digests", + ), + ) + for item in ( + _object(rule, field="world_model.rules[]") + for rule in _array(payload.get("rules"), field="world_model.rules") + ) + ) + return WorldModel(facts=facts, rules=rules) + + +def _restore_actions(value: JsonValue) -> tuple[ActionSpec, ...]: + return tuple( + ActionSpec.create( + action_id=_text(item.get("action_id"), field="action.action_id"), + description=_text(item.get("description"), field="action.description"), + preconditions=tuple( + _restore_pattern(condition, field="action.preconditions[]") + for condition in _array( + item.get("preconditions"), + field="action.preconditions", + ) + ), + effects=tuple( + FactEffect.create( + subject=_text(effect.get("subject"), field="effect.subject"), + predicate=_text( + effect.get("predicate"), + field="effect.predicate", + ), + value=value_from_payload(effect.get("value")), + ) + for effect in ( + _object(raw, field="action.effects[]") + for raw in _array(item.get("effects"), field="action.effects") + ) + ), + cost=_number(item.get("cost"), field="action.cost"), + risk=_number(item.get("risk"), field="action.risk"), + authority_required=_boolean( + item.get("authority_required"), + field="action.authority_required", + ), + ) + for item in ( + _object(action, field="action_catalog[]") + for action in _array(value, field="action_catalog") + ) + ) + + +def _restore_learning(value: JsonValue) -> LearningLedger: + payload = _object(value, field="learning") + outcomes = tuple( + LearningOutcome.create( + outcome_id=_text(item.get("outcome_id"), field="learning.outcome_id"), + skill_id=_text(item.get("skill_id"), field="learning.skill_id"), + task_family=_text( + item.get("task_family"), + field="learning.task_family", + ), + status=OutcomeStatus(_text(item.get("status"), field="learning.status")), + score=_number(item.get("score"), field="learning.score"), + evidence_digest=_digest( + item.get("evidence_digest"), + field="learning.evidence_digest", + ), + notes=_text(item.get("notes"), field="learning.notes"), + ) + for item in ( + _object(outcome, field="learning.outcomes[]") + for outcome in _array(payload.get("outcomes"), field="learning.outcomes") + ) + ) + profiles = tuple( + SkillProfile( + skill_id=CanonicalKey.from_text( + _text(item.get("skill_id"), field="learning.profile.skill_id"), + field_name="skill_id", + ), + attempts=_integer( + item.get("attempts"), + field="learning.profile.attempts", + ), + successes=_integer( + item.get("successes"), + field="learning.profile.successes", + ), + mean_score=_number( + item.get("mean_score"), + field="learning.profile.mean_score", + ), + confidence=_number( + item.get("confidence"), + field="learning.profile.confidence", + ), + last_outcome_digest=( + None + if item.get("last_outcome_digest") is None + else _digest( + item.get("last_outcome_digest"), + field="learning.profile.last_outcome_digest", + ) + ), + ) + for item in ( + _object(profile, field="learning.profiles[]") + for profile in _array(payload.get("profiles"), field="learning.profiles") + ) + ) + return LearningLedger(outcomes=outcomes, profiles=profiles) + + +def _restore_self_model(value: JsonValue) -> SelfModel: + payload = _object(value, field="self_model") + return SelfModel( + tuple( + CapabilityMeasure.create( + capability_id=_text( + item.get("capability_id"), + field="self_model.capability_id", + ), + score=_number(item.get("score"), field="self_model.score"), + evidence_digests=_digests( + item.get("evidence_digests"), + field="self_model.evidence_digests", + ), + limitation=_text( + item.get("limitation"), + field="self_model.limitation", + ), + ) + for item in ( + _object(measure, field="self_model.measures[]") + for measure in _array( + payload.get("measures"), + field="self_model.measures", + ) + ) + ) + ) + + +def _restore_goals(value: JsonValue) -> GoalGraph: + payload = _object(value, field="goals") + goals = tuple( + GoalSpec.create( + goal_id=_text(item.get("goal_id"), field="goals.goal_id"), + description=_text( + item.get("description"), + field="goals.description", + ), + desired_state=_restore_pattern( + item.get("desired_state"), + field="goals.desired_state", + ), + priority=_number(item.get("priority"), field="goals.priority"), + utility=_number(item.get("utility"), field="goals.utility"), + risk_limit=_number( + item.get("risk_limit"), + field="goals.risk_limit", + ), + status=GoalStatus(_text(item.get("status"), field="goals.status")), + dependency_ids=_texts( + item.get("dependency_ids"), + field="goals.dependency_ids", + ), + authority_required=_boolean( + item.get("authority_required"), + field="goals.authority_required", + ), + evidence_digests=_digests( + item.get("evidence_digests"), + field="goals.evidence_digests", + ), + status_reason=_optional_text( + item.get("status_reason"), + field="goals.status_reason", + ), + ) + for item in ( + _object(goal, field="goals.goals[]") + for goal in _array(payload.get("goals"), field="goals.goals") + ) + ) + return GoalGraph.create(goals) + + +def _restore_uncertainty(value: JsonValue) -> UncertaintyLedger: + payload = _object(value, field="uncertainty") + observations = tuple( + CalibrationObservation.create( + observation_id=_text( + item.get("observation_id"), + field="uncertainty.observation_id", + ), + capability_id=_text( + item.get("capability_id"), + field="uncertainty.capability_id", + ), + predicted_probability=_number( + item.get("predicted_probability"), + field="uncertainty.predicted_probability", + ), + observed=_boolean( + item.get("observed"), + field="uncertainty.observed", + ), + evidence_digest=_digest( + item.get("evidence_digest"), + field="uncertainty.evidence_digest", + ), + context=_text(item.get("context"), field="uncertainty.context"), + ) + for item in ( + _object(observation, field="uncertainty.observations[]") + for observation in _array( + payload.get("observations"), + field="uncertainty.observations", + ) + ) + ) + return UncertaintyLedger.create(observations) + + +def _restore_episodes(value: JsonValue) -> EpisodeLedger: + payload = _object(value, field="episodes") + episodes = tuple( + CognitiveEpisode.create( + episode_id=_text(item.get("episode_id"), field="episodes.episode_id"), + sequence=_integer(item.get("sequence"), field="episodes.sequence"), + task=_text(item.get("task"), field="episodes.task"), + initial_state_digest=_digest( + item.get("initial_state_digest"), + field="episodes.initial_state_digest", + ), + final_state_digest=_digest( + item.get("final_state_digest"), + field="episodes.final_state_digest", + ), + steps=tuple( + EpisodeStep.create( + index=_integer(step.get("index"), field="episodes.step.index"), + kind=EpisodeStepKind(_text(step.get("kind"), field="episodes.step.kind")), + status=EpisodeStepStatus( + _text(step.get("status"), field="episodes.step.status") + ), + detail=_text( + step.get("detail"), + field="episodes.step.detail", + ), + input_digests=_digests( + step.get("input_digests"), + field="episodes.step.input_digests", + ), + output_digests=_digests( + step.get("output_digests"), + field="episodes.step.output_digests", + ), + ) + for step in ( + _object(raw_step, field="episodes.steps[]") + for raw_step in _array( + item.get("steps"), + field="episodes.steps", + ) + ) + ), + previous_episode_digest=( + None + if item.get("previous_episode_digest") is None + else _digest( + item.get("previous_episode_digest"), + field="episodes.previous_episode_digest", + ) + ), + ) + for item in ( + _object(episode, field="episodes.episodes[]") + for episode in _array(payload.get("episodes"), field="episodes.episodes") + ) + ) + return EpisodeLedger.create(episodes) + + +def _restore_curriculum(value: JsonValue) -> CurriculumLedger | None: + if value is None: + return None + payload = _object(value, field="curriculum") + curriculum_payload = _object( + payload.get("curriculum"), + field="curriculum.curriculum", + ) + tasks = tuple( + CurriculumTask.create( + task_id=_text(item.get("task_id"), field="curriculum.task_id"), + family=_text(item.get("family"), field="curriculum.family"), + description=_text( + item.get("description"), + field="curriculum.description", + ), + difficulty=_integer( + item.get("difficulty"), + field="curriculum.difficulty", + ), + split=CurriculumSplit(_text(item.get("split"), field="curriculum.split")), + prerequisite_ids=_texts( + item.get("prerequisite_ids"), + field="curriculum.prerequisite_ids", + ), + required_capabilities=_texts( + item.get("required_capabilities"), + field="curriculum.required_capabilities", + ), + pass_threshold=_number( + item.get("pass_threshold"), + field="curriculum.pass_threshold", + ), + ) + for item in ( + _object(task, field="curriculum.tasks[]") + for task in _array( + curriculum_payload.get("tasks"), + field="curriculum.tasks", + ) + ) + ) + curriculum = Curriculum.create(tasks) + trials = tuple( + CurriculumTrial.create( + trial_id=_text(item.get("trial_id"), field="curriculum.trial_id"), + task_id=_text(item.get("task_id"), field="curriculum.trial_task_id"), + sequence=_integer( + item.get("sequence"), + field="curriculum.trial_sequence", + ), + score=_number(item.get("score"), field="curriculum.trial_score"), + status=TrialStatus(_text(item.get("status"), field="curriculum.trial_status")), + evidence_digest=_digest( + item.get("evidence_digest"), + field="curriculum.trial_evidence", + ), + notes=_text(item.get("notes"), field="curriculum.trial_notes"), + ) + for item in ( + _object(trial, field="curriculum.trials[]") + for trial in _array(payload.get("trials"), field="curriculum.trials") + ) + ) + return CurriculumLedger(curriculum, trials) + + +def _restore_primitives(value: JsonValue) -> PrimitiveRegistry: + payload = _object(value, field="primitive_registry") + return PrimitiveRegistry.create( + PrimitiveSpec.create( + primitive_id=_text(item.get("primitive_id"), field="primitive.id"), + kind=PrimitiveKind(_text(item.get("kind"), field="primitive.kind")), + operation=PrimitiveOperation(_text(item.get("operation"), field="primitive.operation")), + arity=_integer(item.get("arity"), field="primitive.arity"), + status=PrimitiveStatus(_text(item.get("status"), field="primitive.status")), + description=_text( + item.get("description"), + field="primitive.description", + ), + grounding_digests=_digests( + item.get("grounding_digests"), + field="primitive.grounding_digests", + ), + validation_digests=_digests( + item.get("validation_digests"), + field="primitive.validation_digests", + ), + reason=_optional_text(item.get("reason"), field="primitive.reason"), + ) + for item in ( + _object(primitive, field="primitive_registry.primitives[]") + for primitive in _array( + payload.get("primitives"), + field="primitive_registry.primitives", + ) + ) + ) + + +def restore_system_state(snapshot: CognitiveSnapshot) -> RestoredCognitiveState: + """Restore and revalidate every serialized IX-Sally cognitive subsystem.""" + state = snapshot.state + if _text(state.get("repository"), field="repository") != "IX-Sally": + raise FoundationError("snapshot state repository mismatch") + runtime_memories: dict[str, CognitiveValue] = {} + for raw in _array(state.get("runtime_memories"), field="runtime_memories"): + item = _object(raw, field="runtime_memories[]") + name = _text(item.get("name"), field="runtime_memories.name") + if name in runtime_memories: + raise FoundationError(f"duplicate runtime memory name: {name}") + runtime_memories[name] = value_from_payload(item.get("value")) + return RestoredCognitiveState( + workspace=_restore_workspace(state.get("workspace")), + active_memory=_restore_memory(state.get("active_memory")), + world_model=_restore_world(state.get("world_model")), + action_catalog=_restore_actions(state.get("action_catalog")), + learning=_restore_learning(state.get("learning")), + self_model=_restore_self_model(state.get("self_model")), + goals=_restore_goals(state.get("goals")), + uncertainty=_restore_uncertainty(state.get("uncertainty")), + episodes=_restore_episodes(state.get("episodes")), + curriculum=_restore_curriculum(state.get("curriculum")), + primitive_registry=_restore_primitives(state.get("primitive_registry")), + runtime_memories=runtime_memories, + execution_count=_integer( + state.get("execution_count"), + field="execution_count", + ), + cycle_count=_integer(state.get("cycle_count"), field="cycle_count"), + ) diff --git a/src/ix_sally/cognition/storage.py b/src/ix_sally/cognition/storage.py new file mode 100644 index 0000000..b4c21fb --- /dev/null +++ b/src/ix_sally/cognition/storage.py @@ -0,0 +1,173 @@ +"""Atomic local snapshot storage with backup validation and explicit recovery.""" + +from __future__ import annotations + +import os +from contextlib import suppress +from dataclasses import dataclass +from enum import StrEnum +from pathlib import Path + +from ix_sally.cognition.persistence import CognitiveSnapshot +from ix_sally.digest import DigestRecord, JsonObject +from ix_sally.foundation import FoundationError + + +class SnapshotSource(StrEnum): + """Source selected while loading a persisted cognitive snapshot.""" + + PRIMARY = "primary" + BACKUP = "backup" + + +@dataclass(frozen=True, slots=True) +class SnapshotSaveReceipt: + """Receipt for one atomic primary-file replacement attempt.""" + + path: str + backup_path: str + snapshot_digest: DigestRecord + bytes_written: int + backup_created: bool + + def to_payload(self) -> JsonObject: + """Return a canonical save receipt.""" + return { + "path": self.path, + "backup_path": self.backup_path, + "snapshot_digest": { + "algorithm": self.snapshot_digest.algorithm, + "value": self.snapshot_digest.value, + }, + "bytes_written": self.bytes_written, + "backup_created": self.backup_created, + } + + def digest(self) -> DigestRecord: + """Return a deterministic receipt identity.""" + return DigestRecord.from_payload(self.to_payload()) + + +@dataclass(frozen=True, slots=True) +class SnapshotLoadResult: + """Validated load result that records whether recovery used a backup.""" + + snapshot: CognitiveSnapshot + source: SnapshotSource + primary_error: str | None = None + + def to_payload(self) -> JsonObject: + """Return a canonical load/recovery result.""" + return { + "snapshot": self.snapshot.to_payload(), + "source": self.source.value, + "primary_error": self.primary_error, + } + + def digest(self) -> DigestRecord: + """Return a deterministic load-result identity.""" + return DigestRecord.from_payload(self.to_payload()) + + +@dataclass(frozen=True, slots=True) +class SnapshotRepository: + """Persist snapshots with temporary-file replacement and one validated backup.""" + + path: Path + + def __post_init__(self) -> None: + """Require a file path rather than an existing directory.""" + if self.path.exists() and self.path.is_dir(): + raise FoundationError("snapshot path must not be a directory") + + @property + def backup_path(self) -> Path: + """Return the deterministic sibling backup path.""" + return self.path.with_name(f"{self.path.name}.bak") + + @property + def temporary_path(self) -> Path: + """Return the deterministic sibling temporary path.""" + return self.path.with_name(f"{self.path.name}.tmp") + + def save(self, snapshot: CognitiveSnapshot) -> SnapshotSaveReceipt: + """Validate, write, flush, and atomically replace the primary snapshot. + + The method uses operating-system replacement semantics. It does not claim + storage-hardware durability beyond the completed writes and flush requests. + """ + encoded = snapshot.to_json().encode("utf-8") + parent = self.path.parent + parent.mkdir(parents=True, exist_ok=True) + backup_created = False + if self.temporary_path.exists(): + self.temporary_path.unlink() + try: + with self.temporary_path.open("xb") as handle: + handle.write(encoded) + handle.flush() + os.fsync(handle.fileno()) + restored = CognitiveSnapshot.from_json(self.temporary_path.read_text(encoding="utf-8")) + if restored.state_digest != snapshot.state_digest: + raise FoundationError("temporary snapshot verification failed") + if self.path.exists(): + current = CognitiveSnapshot.from_json(self.path.read_text(encoding="utf-8")) + backup_encoded = current.to_json().encode("utf-8") + with self.backup_path.open("wb") as handle: + handle.write(backup_encoded) + handle.flush() + os.fsync(handle.fileno()) + backup_created = True + self.temporary_path.replace(self.path) + self._flush_directory(parent) + finally: + if self.temporary_path.exists(): + self.temporary_path.unlink() + loaded = CognitiveSnapshot.from_json(self.path.read_text(encoding="utf-8")) + if loaded.state_digest != snapshot.state_digest: + raise FoundationError("persisted snapshot verification failed") + return SnapshotSaveReceipt( + path=self.path.as_posix(), + backup_path=self.backup_path.as_posix(), + snapshot_digest=snapshot.state_digest, + bytes_written=len(encoded), + backup_created=backup_created, + ) + + def load(self, *, allow_backup: bool = True) -> SnapshotLoadResult: + """Load the primary snapshot or a separately validated backup.""" + primary_error: str | None = None + try: + snapshot = CognitiveSnapshot.from_json(self.path.read_text(encoding="utf-8")) + return SnapshotLoadResult(snapshot, SnapshotSource.PRIMARY) + except (OSError, UnicodeError, FoundationError) as exc: + primary_error = f"{type(exc).__name__}: {exc}" + if not allow_backup: + raise FoundationError(f"primary cognitive snapshot failed: {primary_error}") + try: + snapshot = CognitiveSnapshot.from_json(self.backup_path.read_text(encoding="utf-8")) + except (OSError, UnicodeError, FoundationError) as exc: + backup_error = f"{type(exc).__name__}: {exc}" + raise FoundationError( + "primary and backup cognitive snapshots failed: " + f"primary={primary_error}; backup={backup_error}" + ) from exc + return SnapshotLoadResult( + snapshot=snapshot, + source=SnapshotSource.BACKUP, + primary_error=primary_error, + ) + + @staticmethod + def _flush_directory(directory: Path) -> None: + """Best-effort flush of directory metadata on platforms that support it.""" + flags = getattr(os, "O_DIRECTORY", 0) | os.O_RDONLY + try: + descriptor = os.open(directory, flags) + except OSError: + return + try: + with suppress(OSError): + os.fsync(descriptor) + finally: + os.close(descriptor) diff --git a/src/ix_sally/cognition/system.py b/src/ix_sally/cognition/system.py new file mode 100644 index 0000000..1959f33 --- /dev/null +++ b/src/ix_sally/cognition/system.py @@ -0,0 +1,317 @@ +"""Integrated IX-Sally experimental general-intelligence research runtime.""" + +from __future__ import annotations + +from collections.abc import Iterable +from dataclasses import dataclass, field + +from ix_sally.cognition.active_memory import ( + ActiveMemoryEntry, + ActiveMemoryStore, +) +from ix_sally.cognition.compiler import compile_ix_source +from ix_sally.cognition.curriculum import CurriculumLedger, CurriculumTrial +from ix_sally.cognition.episodes import CognitiveEpisode, EpisodeLedger +from ix_sally.cognition.executive import ExecutiveController, ExecutiveDecision +from ix_sally.cognition.goals import GoalGraph, GoalSpec, GoalStatus +from ix_sally.cognition.governance_bridge import ( + CognitiveProposalBridge, + CognitiveProposalBridgeResult, +) +from ix_sally.cognition.learning import LearningLedger, LearningOutcome +from ix_sally.cognition.metacognition import CapabilityMeasure, SelfModel +from ix_sally.cognition.ninefold import NinefoldCognitiveCycle, NinefoldCoordinator +from ix_sally.cognition.persistence import CognitiveSnapshot +from ix_sally.cognition.planning import ( + ActionSpec, + DeterministicPlanner, + Plan, + PlanExecutionReceipt, + PlanSimulator, +) +from ix_sally.cognition.primitives import ( + PrimitiveExecution, + PrimitiveExecutor, + PrimitiveRegistry, + default_primitive_registry, +) +from ix_sally.cognition.uncertainty import ( + CalibrationObservation, + CalibrationReport, + UncertaintyLedger, +) +from ix_sally.cognition.values import CognitiveValue +from ix_sally.cognition.vm import IXVirtualMachine, VMResult, VMStatus +from ix_sally.cognition.workspace import CognitiveWorkspace, WorkspaceItem +from ix_sally.cognition.world_model import CausalRule, FactPattern, WorldFact, WorldModel +from ix_sally.digest import DigestRecord, JsonArray, JsonObject +from ix_sally.foundation import FoundationError + + +@dataclass(slots=True) +class SallyCognitiveSystem: + """One integrated, bounded cognitive runtime with explicit state transitions.""" + + workspace: CognitiveWorkspace = field(default_factory=CognitiveWorkspace) + active_memory: ActiveMemoryStore = field(default_factory=ActiveMemoryStore) + world_model: WorldModel = field(default_factory=WorldModel) + action_catalog: tuple[ActionSpec, ...] = () + learning: LearningLedger = field(default_factory=LearningLedger) + self_model: SelfModel = field(default_factory=SelfModel) + goals: GoalGraph = field(default_factory=GoalGraph) + uncertainty: UncertaintyLedger = field(default_factory=UncertaintyLedger) + episodes: EpisodeLedger = field(default_factory=EpisodeLedger) + curriculum: CurriculumLedger | None = None + primitive_registry: PrimitiveRegistry = field(default_factory=default_primitive_registry) + runtime_memories: dict[str, CognitiveValue] = field(default_factory=dict) + execution_count: int = 0 + cycle_count: int = 0 + + def __post_init__(self) -> None: + """Require counters and action identities to remain valid.""" + if self.execution_count < 0 or self.cycle_count < 0: + raise FoundationError("system counters must not be negative") + action_ids = [action.action_id.value for action in self.action_catalog] + if len(action_ids) != len(set(action_ids)): + raise FoundationError("system action catalog contains duplicate identifiers") + + @classmethod + def create(cls) -> SallyCognitiveSystem: + """Create a clean IX-Sally cognitive runtime.""" + return cls() + + @classmethod + def from_snapshot(cls, snapshot: CognitiveSnapshot) -> SallyCognitiveSystem: + """Restore a complete system from a verified canonical snapshot.""" + from ix_sally.cognition.restore import restore_system_state + + restored = restore_system_state(snapshot) + system = cls( + workspace=restored.workspace, + active_memory=restored.active_memory, + world_model=restored.world_model, + action_catalog=restored.action_catalog, + learning=restored.learning, + self_model=restored.self_model, + goals=restored.goals, + uncertainty=restored.uncertainty, + episodes=restored.episodes, + curriculum=restored.curriculum, + primitive_registry=restored.primitive_registry, + runtime_memories=restored.runtime_memories, + execution_count=restored.execution_count, + cycle_count=restored.cycle_count, + ) + if system.state_payload() != snapshot.state: + raise FoundationError("restored cognitive system does not match snapshot state") + return system + + def execute_ix( + self, + source: str, + *, + filename: str = "", + max_steps: int = 10_000, + ) -> VMResult: + """Compile and execute IX source, committing memory only after a clean halt.""" + program = compile_ix_source(source, filename=filename) + result = IXVirtualMachine(max_steps=max_steps).execute( + program, + memories=self.runtime_memories, + ) + self.execution_count += 1 + if result.status is VMStatus.HALTED: + self.runtime_memories = result.memory_map() + return result + + def execute_primitive( + self, + primitive_id: str, + inputs: Iterable[CognitiveValue], + ) -> PrimitiveExecution: + """Execute one validated cognitive primitive.""" + return PrimitiveExecutor(self.primitive_registry).execute(primitive_id, inputs) + + def admit_workspace(self, item: WorkspaceItem) -> None: + """Admit one item under workspace capacity and attention policy.""" + self.workspace = self.workspace.admit(item) + + def append_memory(self, entry: ActiveMemoryEntry) -> None: + """Append one active-memory entry under its truth-boundary rules.""" + self.active_memory = self.active_memory.append(entry) + + def observe(self, fact: WorldFact) -> None: + """Append one world fact.""" + self.world_model = self.world_model.observe(fact) + + def add_causal_rule(self, rule: CausalRule) -> None: + """Append one evidence-bound causal rule.""" + self.world_model = self.world_model.add_rule(rule) + + def infer_world(self) -> None: + """Apply all currently satisfied causal rules once.""" + self.world_model = self.world_model.infer() + + def register_action(self, action: ActionSpec) -> None: + """Add one unique declarative planning action.""" + if any(existing.action_id == action.action_id for existing in self.action_catalog): + raise FoundationError(f"planning action already exists: {action.action_id.value}") + self.action_catalog = tuple( + sorted((*self.action_catalog, action), key=lambda item: item.action_id.value) + ) + + def plan(self, goal: FactPattern) -> Plan: + """Build a bounded deterministic plan against the current world model.""" + return DeterministicPlanner().plan( + world_model=self.world_model, + actions=self.action_catalog, + goal=goal, + ) + + def simulate_plan( + self, + plan: Plan, + *, + human_approved: bool = False, + retain_simulation: bool = False, + ) -> PlanExecutionReceipt: + """Simulate plan effects and optionally retain the hypothetical branch.""" + receipt = PlanSimulator().execute( + plan, + world_model=self.world_model, + human_approved=human_approved, + ) + if retain_simulation and receipt.permission.value == "allowed": + self.world_model = receipt.resulting_model + return receipt + + def record_learning(self, outcome: LearningOutcome) -> None: + """Record one evidence-bound learning outcome.""" + self.learning = self.learning.record(outcome) + + def measure_capability(self, measure: CapabilityMeasure) -> None: + """Update the evidence-bound self model.""" + self.self_model = self.self_model.update(measure) + + def register_goal(self, goal: GoalSpec) -> None: + """Add one unique bounded goal.""" + self.goals = self.goals.add(goal) + + def update_goal_status( + self, + goal_id: str, + status: GoalStatus, + *, + reason: str | None = None, + ) -> None: + """Record one explicit goal lifecycle transition.""" + self.goals = self.goals.update_status(goal_id, status, reason=reason) + + def record_calibration(self, observation: CalibrationObservation) -> None: + """Append one forecast/outcome pair to the uncertainty ledger.""" + self.uncertainty = self.uncertainty.record(observation) + + def calibration_report( + self, + *, + capability_id: str | None = None, + bin_count: int = 10, + ) -> CalibrationReport: + """Return transparent confidence calibration metrics.""" + return self.uncertainty.report( + capability_id=capability_id, + bin_count=bin_count, + ) + + def set_curriculum(self, curriculum: CurriculumLedger) -> None: + """Install one explicit curriculum ledger.""" + self.curriculum = curriculum + + def record_curriculum_trial(self, trial: CurriculumTrial) -> None: + """Append one observed curriculum trial.""" + if self.curriculum is None: + raise FoundationError("cannot record a trial without a curriculum") + self.curriculum = self.curriculum.record(trial) + + def deliberate( + self, + *, + task: str, + use_calibration_gate: bool = True, + ) -> ExecutiveDecision: + """Produce one bounded executive decision without executing it.""" + calibration = self.uncertainty.report() if use_calibration_gate else None + return ExecutiveController().deliberate( + task=task, + goals=self.goals, + workspace=self.workspace, + memory=self.active_memory, + world_model=self.world_model, + actions=self.action_catalog, + calibration=calibration, + ) + + def bridge_decision( + self, + decision: ExecutiveDecision, + *, + cycle: int, + ) -> CognitiveProposalBridgeResult: + """Convert a plan proposal into the existing IX-Sally control plane.""" + return CognitiveProposalBridge().bridge(decision=decision, cycle=cycle) + + def append_episode(self, episode: CognitiveEpisode) -> None: + """Append one fully linked replayable cognitive episode.""" + self.episodes = self.episodes.append(episode) + + def run_cycle( + self, + *, + task: str, + goal: FactPattern | None = None, + ) -> NinefoldCognitiveCycle: + """Run one complete functional ninefold cognitive cycle.""" + cycle = NinefoldCoordinator().run( + task=task, + workspace=self.workspace, + memory=self.active_memory, + world_model=self.world_model, + learning=self.learning, + actions=self.action_catalog, + goal=goal, + ) + self.cycle_count += 1 + return cycle + + def state_payload(self) -> JsonObject: + """Return a complete canonical state representation.""" + runtime_memories: JsonArray = [ + {"name": name, "value": value.to_payload()} + for name, value in sorted(self.runtime_memories.items()) + ] + actions: JsonArray = [action.to_payload() for action in self.action_catalog] + return { + "repository": "IX-Sally", + "workspace": self.workspace.to_payload(), + "active_memory": self.active_memory.to_payload(), + "world_model": self.world_model.to_payload(), + "action_catalog": actions, + "learning": self.learning.to_payload(), + "self_model": self.self_model.to_payload(), + "goals": self.goals.to_payload(), + "uncertainty": self.uncertainty.to_payload(), + "episodes": self.episodes.to_payload(), + "curriculum": (self.curriculum.to_payload() if self.curriculum is not None else None), + "primitive_registry": self.primitive_registry.to_payload(), + "runtime_memories": runtime_memories, + "execution_count": self.execution_count, + "cycle_count": self.cycle_count, + } + + def digest(self) -> DigestRecord: + """Return a deterministic identity for the complete cognitive state.""" + return DigestRecord.from_payload(self.state_payload()) + + def snapshot(self) -> CognitiveSnapshot: + """Return a tamper-evident complete state snapshot.""" + return CognitiveSnapshot.create(self.state_payload()) diff --git a/src/ix_sally/cognition/uncertainty.py b/src/ix_sally/cognition/uncertainty.py new file mode 100644 index 0000000..6ed90bb --- /dev/null +++ b/src/ix_sally/cognition/uncertainty.py @@ -0,0 +1,224 @@ +"""Evidence-bound confidence calibration and decision-threshold records.""" + +from __future__ import annotations + +from collections.abc import Iterable +from dataclasses import dataclass + +from ix_sally.digest import DigestRecord, JsonArray, JsonObject +from ix_sally.foundation import CanonicalKey, FoundationError, require_text + + +@dataclass(frozen=True, slots=True) +class CalibrationObservation: + """One probability forecast paired with an independently observed outcome.""" + + observation_id: CanonicalKey + capability_id: CanonicalKey + predicted_probability: float + observed: bool + evidence_digest: DigestRecord + context: str + + @classmethod + def create( + cls, + *, + observation_id: str, + capability_id: str, + predicted_probability: float, + observed: bool, + evidence_digest: DigestRecord, + context: str, + ) -> CalibrationObservation: + """Create a calibration observation without altering the forecast.""" + if not 0.0 <= predicted_probability <= 1.0: + raise FoundationError("predicted probability must be between 0 and 1") + evidence_digest.require_algorithm("sha256") + return cls( + observation_id=CanonicalKey.from_text( + observation_id, + field_name="observation_id", + ), + capability_id=CanonicalKey.from_text( + capability_id, + field_name="capability_id", + ), + predicted_probability=predicted_probability, + observed=observed, + evidence_digest=evidence_digest, + context=require_text(context, field_name="context"), + ) + + def squared_error(self) -> float: + """Return the Brier-score contribution for this forecast.""" + outcome = 1.0 if self.observed else 0.0 + return round((self.predicted_probability - outcome) ** 2, 12) + + def to_payload(self) -> JsonObject: + """Return a canonical forecast/outcome payload.""" + return { + "observation_id": self.observation_id.value, + "capability_id": self.capability_id.value, + "predicted_probability": self.predicted_probability, + "observed": self.observed, + "evidence_digest": { + "algorithm": self.evidence_digest.algorithm, + "value": self.evidence_digest.value, + }, + "context": self.context, + "squared_error": self.squared_error(), + } + + def digest(self) -> DigestRecord: + """Return a deterministic observation identity.""" + return DigestRecord.from_payload(self.to_payload()) + + +@dataclass(frozen=True, slots=True) +class CalibrationBin: + """One fixed-width confidence bin used by a calibration report.""" + + lower_bound: float + upper_bound: float + count: int + mean_prediction: float + observed_frequency: float + absolute_gap: float + + def to_payload(self) -> JsonObject: + """Return a canonical bin payload.""" + return { + "lower_bound": self.lower_bound, + "upper_bound": self.upper_bound, + "count": self.count, + "mean_prediction": self.mean_prediction, + "observed_frequency": self.observed_frequency, + "absolute_gap": self.absolute_gap, + } + + +@dataclass(frozen=True, slots=True) +class CalibrationReport: + """Deterministic calibration metrics for one capability or the complete ledger.""" + + observation_count: int + brier_score: float + expected_calibration_error: float + bins: tuple[CalibrationBin, ...] + + def __post_init__(self) -> None: + """Require internally consistent metric ranges.""" + if self.observation_count < 0: + raise FoundationError("calibration observation count must not be negative") + if not 0.0 <= self.brier_score <= 1.0: + raise FoundationError("calibration Brier score must be between 0 and 1") + if not 0.0 <= self.expected_calibration_error <= 1.0: + raise FoundationError("calibration error must be between 0 and 1") + if sum(item.count for item in self.bins) != self.observation_count: + raise FoundationError("calibration bin counts must equal observation count") + + def to_payload(self) -> JsonObject: + """Return a canonical calibration-report payload.""" + bins: JsonArray = [item.to_payload() for item in self.bins] + return { + "observation_count": self.observation_count, + "brier_score": self.brier_score, + "expected_calibration_error": self.expected_calibration_error, + "bins": bins, + } + + def digest(self) -> DigestRecord: + """Return a deterministic report identity.""" + return DigestRecord.from_payload(self.to_payload()) + + +@dataclass(frozen=True, slots=True) +class UncertaintyLedger: + """Immutable forecast ledger with transparent calibration calculations.""" + + observations: tuple[CalibrationObservation, ...] = () + + def __post_init__(self) -> None: + """Reject duplicate observation identifiers.""" + identifiers = [item.observation_id.value for item in self.observations] + if len(identifiers) != len(set(identifiers)): + raise FoundationError("uncertainty ledger contains duplicate observations") + + @classmethod + def create( + cls, + observations: Iterable[CalibrationObservation] = (), + ) -> UncertaintyLedger: + """Create a ledger in stable identifier order.""" + return cls(tuple(sorted(observations, key=lambda item: item.observation_id.value))) + + def record(self, observation: CalibrationObservation) -> UncertaintyLedger: + """Return a ledger with one unique forecast/outcome pair appended.""" + return UncertaintyLedger.create((*self.observations, observation)) + + def for_capability(self, capability_id: str) -> tuple[CalibrationObservation, ...]: + """Return observations for one canonical capability.""" + requested = CanonicalKey.from_text(capability_id, field_name="capability_id") + return tuple(item for item in self.observations if item.capability_id == requested) + + def report( + self, + *, + capability_id: str | None = None, + bin_count: int = 10, + ) -> CalibrationReport: + """Calculate Brier score and fixed-bin expected calibration error.""" + if bin_count < 1 or bin_count > 100: + raise FoundationError("calibration bin_count must be between 1 and 100") + selected = ( + self.observations if capability_id is None else self.for_capability(capability_id) + ) + if not selected: + return CalibrationReport(0, 0.0, 0.0, ()) + bins: list[CalibrationBin] = [] + width = 1.0 / bin_count + weighted_gap = 0.0 + for index in range(bin_count): + lower = index * width + upper = (index + 1) * width + members = tuple( + item + for item in selected + if lower <= item.predicted_probability + and (item.predicted_probability < upper or index == bin_count - 1) + ) + if not members: + continue + mean_prediction = sum(item.predicted_probability for item in members) / len(members) + observed_frequency = sum(1.0 if item.observed else 0.0 for item in members) / len( + members + ) + gap = abs(mean_prediction - observed_frequency) + weighted_gap += gap * len(members) + bins.append( + CalibrationBin( + lower_bound=round(lower, 12), + upper_bound=round(upper, 12), + count=len(members), + mean_prediction=round(mean_prediction, 12), + observed_frequency=round(observed_frequency, 12), + absolute_gap=round(gap, 12), + ) + ) + brier = sum(item.squared_error() for item in selected) / len(selected) + return CalibrationReport( + observation_count=len(selected), + brier_score=round(brier, 12), + expected_calibration_error=round(weighted_gap / len(selected), 12), + bins=tuple(bins), + ) + + def to_payload(self) -> JsonObject: + """Return a canonical uncertainty-ledger payload.""" + observations: JsonArray = [item.to_payload() for item in self.observations] + return {"observations": observations} + + def digest(self) -> DigestRecord: + """Return a deterministic ledger identity.""" + return DigestRecord.from_payload(self.to_payload()) diff --git a/src/ix_sally/cognition/values.py b/src/ix_sally/cognition/values.py new file mode 100644 index 0000000..29fd1a3 --- /dev/null +++ b/src/ix_sally/cognition/values.py @@ -0,0 +1,118 @@ +"""Typed runtime values used by the IX-Sally cognitive execution core.""" + +from __future__ import annotations + +import math +from dataclasses import dataclass +from enum import StrEnum +from typing import TypeAlias + +from ix_sally.digest import JsonPrimitive, JsonValue +from ix_sally.foundation import FoundationError + +CognitiveScalar: TypeAlias = str | int | float | bool | None + + +class CognitiveValueType(StrEnum): + """Closed set of values accepted by deterministic cognitive execution.""" + + NULL = "null" + BOOLEAN = "boolean" + INTEGER = "integer" + FLOAT = "float" + STRING = "string" + + +@dataclass(frozen=True, slots=True) +class CognitiveValue: + """One validated scalar value with an explicit runtime type.""" + + value_type: CognitiveValueType + value: CognitiveScalar + + @classmethod + def from_python(cls, value: CognitiveScalar) -> CognitiveValue: + """Create a typed value without coercing between Python scalar types.""" + if value is None: + return cls(CognitiveValueType.NULL, None) + if isinstance(value, bool): + return cls(CognitiveValueType.BOOLEAN, value) + if isinstance(value, int): + return cls(CognitiveValueType.INTEGER, value) + if isinstance(value, float): + if not math.isfinite(value): + raise FoundationError("cognitive floating-point values must be finite") + return cls(CognitiveValueType.FLOAT, value) + if isinstance(value, str): + return cls(CognitiveValueType.STRING, value) + raise FoundationError(f"unsupported cognitive value type: {type(value).__name__}") + + def __post_init__(self) -> None: + """Require the declared type to exactly match the stored scalar.""" + expected = _value_type(self.value) + if self.value_type is not expected: + raise FoundationError( + "cognitive value type does not match its stored scalar: " + f"declared {self.value_type.value}, actual {expected.value}" + ) + + def to_payload(self) -> dict[str, JsonValue]: + """Return a deterministic JSON-compatible representation.""" + primitive: JsonPrimitive = self.value + return {"type": self.value_type.value, "value": primitive} + + def require_boolean(self, *, operation: str) -> bool: + """Return the Boolean value or fail closed with operation context.""" + if self.value_type is not CognitiveValueType.BOOLEAN: + raise FoundationError(f"{operation} requires Boolean, got {self.value_type.value}") + assert isinstance(self.value, bool) + return self.value + + def require_numeric(self, *, operation: str) -> int | float: + """Return an integer or float without converting its exact type.""" + if self.value_type not in { + CognitiveValueType.INTEGER, + CognitiveValueType.FLOAT, + }: + raise FoundationError( + f"{operation} requires numeric value, got {self.value_type.value}" + ) + assert isinstance(self.value, int | float) + assert not isinstance(self.value, bool) + return self.value + + +def value_from_payload(payload: JsonValue) -> CognitiveValue: + """Restore one cognitive value from its canonical payload.""" + if not isinstance(payload, dict): + raise FoundationError("cognitive value payload must be an object") + if "type" not in payload or "value" not in payload: + raise FoundationError("cognitive value payload requires type and value fields") + value_type = payload.get("type") + if not isinstance(value_type, str): + raise FoundationError("cognitive value payload requires a text type") + try: + declared = CognitiveValueType(value_type) + except ValueError as exc: + raise FoundationError(f"unknown cognitive value type: {value_type}") from exc + value = payload.get("value") + if not isinstance(value, str | int | float | bool | type(None)): + raise FoundationError("cognitive value payload contains a non-scalar value") + return CognitiveValue(declared, value) + + +def _value_type(value: CognitiveScalar) -> CognitiveValueType: + """Return the exact cognitive type for one supported Python scalar.""" + if value is None: + return CognitiveValueType.NULL + if isinstance(value, bool): + return CognitiveValueType.BOOLEAN + if isinstance(value, int): + return CognitiveValueType.INTEGER + if isinstance(value, float): + if not math.isfinite(value): + raise FoundationError("cognitive floating-point values must be finite") + return CognitiveValueType.FLOAT + if isinstance(value, str): + return CognitiveValueType.STRING + raise FoundationError(f"unsupported cognitive value type: {type(value).__name__}") diff --git a/src/ix_sally/cognition/vm.py b/src/ix_sally/cognition/vm.py new file mode 100644 index 0000000..b95a5cc --- /dev/null +++ b/src/ix_sally/cognition/vm.py @@ -0,0 +1,385 @@ +"""Bounded deterministic virtual machine for compiled IX programs.""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass +from enum import StrEnum + +from ix_sally.cognition.bytecode import BytecodeProgram, Instruction, OpCode +from ix_sally.cognition.values import CognitiveValue, CognitiveValueType +from ix_sally.digest import DigestRecord, JsonArray, JsonObject +from ix_sally.foundation import FoundationError + + +class VMStatus(StrEnum): + """Terminal status of one bounded VM run.""" + + HALTED = "halted" + FAILED = "failed" + STEP_LIMIT = "step_limit" + + +@dataclass(frozen=True, slots=True) +class VMTraceEntry: + """One instruction-level execution receipt.""" + + step: int + instruction_pointer: int + opcode: OpCode + stack_depth_before: int + stack_depth_after: int + + def to_payload(self) -> JsonObject: + """Return a stable trace payload.""" + return { + "step": self.step, + "instruction_pointer": self.instruction_pointer, + "opcode": self.opcode.value, + "stack_depth_before": self.stack_depth_before, + "stack_depth_after": self.stack_depth_after, + } + + +@dataclass(frozen=True, slots=True) +class VMResult: + """Complete immutable receipt from one IX bytecode execution.""" + + status: VMStatus + program_digest: DigestRecord + steps: int + local_values: tuple[tuple[str, CognitiveValue], ...] + memories: tuple[tuple[str, CognitiveValue], ...] + outputs: tuple[CognitiveValue, ...] + replies: tuple[CognitiveValue, ...] + traces: tuple[CognitiveValue, ...] + instruction_trace: tuple[VMTraceEntry, ...] + failure: str | None = None + + def __post_init__(self) -> None: + """Require terminal status and failure detail to agree.""" + self.program_digest.require_algorithm("sha256") + if self.steps < 0: + raise FoundationError("VM result steps must not be negative") + if self.status is VMStatus.HALTED and self.failure is not None: + raise FoundationError("halted VM result must not contain a failure") + if self.status is not VMStatus.HALTED and not self.failure: + raise FoundationError("non-halted VM result requires failure detail") + + def local_map(self) -> dict[str, CognitiveValue]: + """Return a defensive local-value mapping.""" + return dict(self.local_values) + + def memory_map(self) -> dict[str, CognitiveValue]: + """Return a defensive memory-value mapping.""" + return dict(self.memories) + + def to_payload(self) -> JsonObject: + """Return a deterministic receipt payload.""" + local_values: JsonArray = [ + {"name": name, "value": value.to_payload()} for name, value in self.local_values + ] + memory_values: JsonArray = [ + {"name": name, "value": value.to_payload()} for name, value in self.memories + ] + outputs: JsonArray = [value.to_payload() for value in self.outputs] + replies: JsonArray = [value.to_payload() for value in self.replies] + traces: JsonArray = [value.to_payload() for value in self.traces] + instruction_trace: JsonArray = [entry.to_payload() for entry in self.instruction_trace] + return { + "status": self.status.value, + "program_digest": { + "algorithm": self.program_digest.algorithm, + "value": self.program_digest.value, + }, + "steps": self.steps, + "locals": local_values, + "memories": memory_values, + "outputs": outputs, + "replies": replies, + "traces": traces, + "instruction_trace": instruction_trace, + "failure": self.failure, + } + + def digest(self) -> DigestRecord: + """Return a deterministic identity for this execution.""" + return DigestRecord.from_payload(self.to_payload()) + + +@dataclass(frozen=True, slots=True) +class IXVirtualMachine: + """Execute bytecode with no I/O, imports, callbacks, or arbitrary code.""" + + max_steps: int = 10_000 + + def __post_init__(self) -> None: + """Require a positive execution budget.""" + if self.max_steps <= 0: + raise FoundationError("VM max_steps must be positive") + + def execute( + self, + program: BytecodeProgram, + *, + initial_locals: Mapping[str, CognitiveValue] | None = None, + memories: Mapping[str, CognitiveValue] | None = None, + ) -> VMResult: + """Execute one program and return a receipt instead of raising runtime faults.""" + local_values = dict(initial_locals or {}) + memory_values = dict(memories or {}) + stack: list[CognitiveValue] = [] + outputs: list[CognitiveValue] = [] + replies: list[CognitiveValue] = [] + traces: list[CognitiveValue] = [] + instruction_trace: list[VMTraceEntry] = [] + pointer = 0 + steps = 0 + + while pointer < len(program.instructions): + if steps >= self.max_steps: + return self._result( + status=VMStatus.STEP_LIMIT, + program=program, + steps=steps, + local_values=local_values, + memories=memory_values, + outputs=outputs, + replies=replies, + traces=traces, + instruction_trace=instruction_trace, + failure=f"execution exceeded step limit {self.max_steps}", + ) + instruction = program.instructions[pointer] + depth_before = len(stack) + try: + halted = self._execute_instruction( + instruction, + stack=stack, + local_values=local_values, + memories=memory_values, + outputs=outputs, + replies=replies, + traces=traces, + ) + except (FoundationError, ZeroDivisionError) as exc: + return self._result( + status=VMStatus.FAILED, + program=program, + steps=steps + 1, + local_values=local_values, + memories=memory_values, + outputs=outputs, + replies=replies, + traces=traces, + instruction_trace=instruction_trace, + failure=f"instruction {pointer} {instruction.opcode.value}: {exc}", + ) + steps += 1 + instruction_trace.append( + VMTraceEntry( + step=steps, + instruction_pointer=pointer, + opcode=instruction.opcode, + stack_depth_before=depth_before, + stack_depth_after=len(stack), + ) + ) + if halted: + return self._result( + status=VMStatus.HALTED, + program=program, + steps=steps, + local_values=local_values, + memories=memory_values, + outputs=outputs, + replies=replies, + traces=traces, + instruction_trace=instruction_trace, + ) + pointer += 1 + + return self._result( + status=VMStatus.FAILED, + program=program, + steps=steps, + local_values=local_values, + memories=memory_values, + outputs=outputs, + replies=replies, + traces=traces, + instruction_trace=instruction_trace, + failure="program terminated without halt", + ) + + def _execute_instruction( + self, + instruction: Instruction, + *, + stack: list[CognitiveValue], + local_values: dict[str, CognitiveValue], + memories: dict[str, CognitiveValue], + outputs: list[CognitiveValue], + replies: list[CognitiveValue], + traces: list[CognitiveValue], + ) -> bool: + """Execute one instruction and return whether it halted the VM.""" + opcode = instruction.opcode + if opcode is OpCode.HALT: + return True + if opcode is OpCode.PUSH: + assert instruction.value is not None + stack.append(instruction.value) + return False + if opcode is OpCode.LOAD_LOCAL: + assert instruction.name is not None + stack.append(self._require_name(local_values, instruction.name, namespace="local")) + return False + if opcode is OpCode.STORE_LOCAL: + assert instruction.name is not None + local_values[instruction.name] = self._pop(stack, operation=opcode.value) + return False + if opcode is OpCode.LOAD_MEMORY: + assert instruction.name is not None + stack.append(self._require_name(memories, instruction.name, namespace="memory")) + return False + if opcode is OpCode.STORE_MEMORY: + assert instruction.name is not None + memories[instruction.name] = self._pop(stack, operation=opcode.value) + return False + if opcode in { + OpCode.UNARY_POSITIVE, + OpCode.UNARY_NEGATE, + OpCode.UNARY_NOT, + }: + stack.append(self._unary(opcode, self._pop(stack, operation=opcode.value))) + return False + if opcode.value.startswith("binary_"): + right = self._pop(stack, operation=opcode.value) + left = self._pop(stack, operation=opcode.value) + stack.append(self._binary(opcode, left, right)) + return False + value = self._pop(stack, operation=opcode.value) + if opcode is OpCode.EMIT_OUTPUT: + outputs.append(value) + return False + if opcode is OpCode.EMIT_REPLY: + replies.append(value) + return False + if opcode is OpCode.TRACE: + traces.append(value) + return False + if opcode is OpCode.ASSERT: + if not value.require_boolean(operation="assert"): + raise FoundationError("assertion evaluated to false") + return False + raise FoundationError(f"unsupported VM opcode: {opcode.value}") + + def _unary(self, opcode: OpCode, value: CognitiveValue) -> CognitiveValue: + """Evaluate one unary operation without silent coercion.""" + if opcode is OpCode.UNARY_NOT: + return CognitiveValue.from_python(not value.require_boolean(operation=opcode.value)) + number = value.require_numeric(operation=opcode.value) + if opcode is OpCode.UNARY_POSITIVE: + return CognitiveValue.from_python(+number) + return CognitiveValue.from_python(-number) + + def _binary( + self, + opcode: OpCode, + left: CognitiveValue, + right: CognitiveValue, + ) -> CognitiveValue: + """Evaluate one binary operation under exact runtime type rules.""" + if opcode is OpCode.BINARY_EQUAL: + return CognitiveValue.from_python( + left.value_type is right.value_type and left.value == right.value + ) + if opcode is OpCode.BINARY_NOT_EQUAL: + return CognitiveValue.from_python( + left.value_type is not right.value_type or left.value != right.value + ) + if opcode in {OpCode.BINARY_OR, OpCode.BINARY_AND}: + left_bool = left.require_boolean(operation=opcode.value) + right_bool = right.require_boolean(operation=opcode.value) + result = left_bool or right_bool + if opcode is OpCode.BINARY_AND: + result = left_bool and right_bool + return CognitiveValue.from_python(result) + if opcode is OpCode.BINARY_ADD and ( + left.value_type is CognitiveValueType.STRING + or right.value_type is CognitiveValueType.STRING + ): + if left.value_type is not right.value_type: + raise FoundationError("string addition requires two strings") + assert isinstance(left.value, str) + assert isinstance(right.value, str) + return CognitiveValue.from_python(left.value + right.value) + left_number = left.require_numeric(operation=opcode.value) + right_number = right.require_numeric(operation=opcode.value) + if opcode is OpCode.BINARY_ADD: + return CognitiveValue.from_python(left_number + right_number) + if opcode is OpCode.BINARY_SUBTRACT: + return CognitiveValue.from_python(left_number - right_number) + if opcode is OpCode.BINARY_MULTIPLY: + return CognitiveValue.from_python(left_number * right_number) + if opcode is OpCode.BINARY_DIVIDE: + if right_number == 0: + raise ZeroDivisionError("division by zero") + return CognitiveValue.from_python(left_number / right_number) + if opcode is OpCode.BINARY_GREATER: + return CognitiveValue.from_python(left_number > right_number) + if opcode is OpCode.BINARY_GREATER_EQUAL: + return CognitiveValue.from_python(left_number >= right_number) + if opcode is OpCode.BINARY_LESS: + return CognitiveValue.from_python(left_number < right_number) + if opcode is OpCode.BINARY_LESS_EQUAL: + return CognitiveValue.from_python(left_number <= right_number) + raise FoundationError(f"unsupported binary opcode: {opcode.value}") + + def _pop(self, stack: list[CognitiveValue], *, operation: str) -> CognitiveValue: + """Pop one stack value or report deterministic underflow.""" + if not stack: + raise FoundationError(f"stack underflow during {operation}") + return stack.pop() + + def _require_name( + self, + values: Mapping[str, CognitiveValue], + name: str, + *, + namespace: str, + ) -> CognitiveValue: + """Return one bound value or fail closed.""" + try: + return values[name] + except KeyError as exc: + raise FoundationError(f"unknown {namespace} name: {name}") from exc + + def _result( + self, + *, + status: VMStatus, + program: BytecodeProgram, + steps: int, + local_values: Mapping[str, CognitiveValue], + memories: Mapping[str, CognitiveValue], + outputs: list[CognitiveValue], + replies: list[CognitiveValue], + traces: list[CognitiveValue], + instruction_trace: list[VMTraceEntry], + failure: str | None = None, + ) -> VMResult: + """Create a stable terminal VM result.""" + return VMResult( + status=status, + program_digest=program.digest(), + steps=steps, + local_values=tuple(sorted(local_values.items())), + memories=tuple(sorted(memories.items())), + outputs=tuple(outputs), + replies=tuple(replies), + traces=tuple(traces), + instruction_trace=tuple(instruction_trace), + failure=failure, + ) diff --git a/src/ix_sally/cognition/workspace.py b/src/ix_sally/cognition/workspace.py new file mode 100644 index 0000000..8f15c86 --- /dev/null +++ b/src/ix_sally/cognition/workspace.py @@ -0,0 +1,213 @@ +"""Attention, belief, hypothesis, goal, and executive workspace records.""" + +from __future__ import annotations + +from collections.abc import Iterable +from dataclasses import dataclass +from enum import StrEnum + +from ix_sally.digest import DigestRecord, JsonArray, JsonObject +from ix_sally.foundation import CanonicalKey, FoundationError, require_text + + +class WorkspaceItemKind(StrEnum): + """Kinds of information admitted to the cognitive workspace.""" + + OBSERVATION = "observation" + BELIEF = "belief" + HYPOTHESIS = "hypothesis" + GOAL = "goal" + QUESTION = "question" + RISK = "risk" + + +class WorkspaceItemStatus(StrEnum): + """Lifecycle state of one workspace item.""" + + ACTIVE = "active" + SATISFIED = "satisfied" + REJECTED = "rejected" + SUSPENDED = "suspended" + + +@dataclass(frozen=True, slots=True) +class WorkspaceItem: + """One confidence-bounded item with explicit evidence and salience.""" + + item_id: CanonicalKey + kind: WorkspaceItemKind + content: str + confidence: float + salience: float + status: WorkspaceItemStatus = WorkspaceItemStatus.ACTIVE + evidence_digests: tuple[DigestRecord, ...] = () + parent_ids: tuple[CanonicalKey, ...] = () + + @classmethod + def create( + cls, + *, + item_id: str, + kind: WorkspaceItemKind, + content: str, + confidence: float, + salience: float, + status: WorkspaceItemStatus = WorkspaceItemStatus.ACTIVE, + evidence_digests: Iterable[DigestRecord] = (), + parent_ids: Iterable[str] = (), + ) -> WorkspaceItem: + """Create an item while preserving uncertainty and provenance.""" + for field_name, value in { + "confidence": confidence, + "salience": salience, + }.items(): + if not 0.0 <= value <= 1.0: + raise FoundationError(f"workspace {field_name} must be between 0 and 1") + evidence = tuple(evidence_digests) + for digest in evidence: + digest.require_algorithm("sha256") + if kind is WorkspaceItemKind.OBSERVATION and confidence < 1.0: + raise FoundationError("direct observation confidence must be 1.0") + return cls( + item_id=CanonicalKey.from_text(item_id, field_name="item_id"), + kind=kind, + content=require_text(content, field_name="content"), + confidence=confidence, + salience=salience, + status=status, + evidence_digests=evidence, + parent_ids=tuple( + sorted( + { + CanonicalKey.from_text(parent, field_name="parent_id") + for parent in parent_ids + }, + key=lambda item: item.value, + ) + ), + ) + + def attention_score(self) -> float: + """Return the deterministic attention score for this item.""" + status_weight = 1.0 if self.status is WorkspaceItemStatus.ACTIVE else 0.25 + kind_weight = { + WorkspaceItemKind.RISK: 1.0, + WorkspaceItemKind.GOAL: 0.95, + WorkspaceItemKind.QUESTION: 0.9, + WorkspaceItemKind.OBSERVATION: 0.85, + WorkspaceItemKind.HYPOTHESIS: 0.8, + WorkspaceItemKind.BELIEF: 0.75, + }[self.kind] + score = status_weight * kind_weight * (0.65 * self.salience + 0.35 * self.confidence) + return round(score, 12) + + def to_payload(self) -> JsonObject: + """Return a canonical workspace-item payload.""" + evidence: JsonArray = [ + {"algorithm": digest.algorithm, "value": digest.value} + for digest in self.evidence_digests + ] + parents: JsonArray = [parent.value for parent in self.parent_ids] + return { + "item_id": self.item_id.value, + "kind": self.kind.value, + "content": self.content, + "confidence": self.confidence, + "salience": self.salience, + "attention_score": self.attention_score(), + "status": self.status.value, + "evidence_digests": evidence, + "parent_ids": parents, + } + + def digest(self) -> DigestRecord: + """Return a deterministic item identity.""" + return DigestRecord.from_payload(self.to_payload()) + + +@dataclass(frozen=True, slots=True) +class CognitiveWorkspace: + """Immutable bounded workspace with deterministic attention selection.""" + + items: tuple[WorkspaceItem, ...] = () + capacity: int = 32 + + def __post_init__(self) -> None: + """Enforce capacity and identifier uniqueness.""" + if self.capacity <= 0: + raise FoundationError("workspace capacity must be positive") + if len(self.items) > self.capacity: + raise FoundationError("workspace exceeds configured capacity") + identifiers = [item.item_id.value for item in self.items] + if len(identifiers) != len(set(identifiers)): + raise FoundationError("workspace contains duplicate item identifiers") + + def admit(self, item: WorkspaceItem) -> CognitiveWorkspace: + """Return a workspace with one item admitted and low-attention overflow evicted.""" + if any(existing.item_id == item.item_id for existing in self.items): + raise FoundationError(f"workspace item already exists: {item.item_id.value}") + candidates = (*self.items, item) + ordered = tuple( + sorted( + candidates, + key=lambda candidate: ( + -candidate.attention_score(), + candidate.item_id.value, + ), + ) + ) + retained = ordered[: self.capacity] + return CognitiveWorkspace( + items=tuple(sorted(retained, key=lambda candidate: candidate.item_id.value)), + capacity=self.capacity, + ) + + def replace(self, item: WorkspaceItem) -> CognitiveWorkspace: + """Replace one existing item without changing unrelated state.""" + if not any(existing.item_id == item.item_id for existing in self.items): + raise FoundationError(f"unknown workspace item: {item.item_id.value}") + return CognitiveWorkspace( + items=tuple( + item if existing.item_id == item.item_id else existing for existing in self.items + ), + capacity=self.capacity, + ) + + def focus(self, limit: int = 5) -> tuple[WorkspaceItem, ...]: + """Return the highest-attention active items.""" + if limit <= 0: + raise FoundationError("workspace focus limit must be positive") + active = (item for item in self.items if item.status is WorkspaceItemStatus.ACTIVE) + return tuple( + sorted( + active, + key=lambda item: (-item.attention_score(), item.item_id.value), + )[:limit] + ) + + def goals(self) -> tuple[WorkspaceItem, ...]: + """Return active goals in attention order.""" + return tuple( + item for item in self.focus(limit=self.capacity) if item.kind is WorkspaceItemKind.GOAL + ) + + def to_payload(self) -> JsonObject: + """Return a deterministic workspace payload.""" + items: JsonArray = [ + item.to_payload() + for item in sorted( + self.items, + key=lambda candidate: candidate.item_id.value, + ) + ] + focus: JsonArray = [item.item_id.value for item in self.focus()] + return { + "capacity": self.capacity, + "item_count": len(self.items), + "items": items, + "focus": focus, + } + + def digest(self) -> DigestRecord: + """Return a deterministic workspace identity.""" + return DigestRecord.from_payload(self.to_payload()) diff --git a/src/ix_sally/cognition/world_model.py b/src/ix_sally/cognition/world_model.py new file mode 100644 index 0000000..bb2f703 --- /dev/null +++ b/src/ix_sally/cognition/world_model.py @@ -0,0 +1,354 @@ +"""Evidence-aware entities, facts, causal rules, predictions, and counterfactuals.""" + +from __future__ import annotations + +from collections.abc import Iterable, Mapping +from dataclasses import dataclass +from enum import StrEnum + +from ix_sally.cognition.values import CognitiveValue +from ix_sally.digest import DigestRecord, JsonArray, JsonObject, JsonValue +from ix_sally.foundation import CanonicalKey, FoundationError + + +class FactStatus(StrEnum): + """Epistemic status of a fact represented in the world model.""" + + OBSERVED = "observed" + INFERRED = "inferred" + PREDICTED = "predicted" + HYPOTHETICAL = "hypothetical" + CONTRADICTED = "contradicted" + + +@dataclass(frozen=True, slots=True) +class WorldFact: + """One subject-predicate-object assertion with explicit epistemic status.""" + + fact_id: CanonicalKey + subject: CanonicalKey + predicate: CanonicalKey + value: CognitiveValue + status: FactStatus + confidence: float + evidence_digests: tuple[DigestRecord, ...] = () + derived_from: tuple[CanonicalKey, ...] = () + + @classmethod + def create( + cls, + *, + fact_id: str, + subject: str, + predicate: str, + value: CognitiveValue, + status: FactStatus, + confidence: float, + evidence_digests: Iterable[DigestRecord] = (), + derived_from: Iterable[str] = (), + ) -> WorldFact: + """Create a fact without collapsing observation, inference, and prediction.""" + if not 0.0 <= confidence <= 1.0: + raise FoundationError("world fact confidence must be between 0 and 1") + evidence = tuple(evidence_digests) + for digest in evidence: + digest.require_algorithm("sha256") + if status is FactStatus.OBSERVED and not evidence: + raise FoundationError("observed world fact requires evidence") + return cls( + fact_id=CanonicalKey.from_text(fact_id, field_name="fact_id"), + subject=CanonicalKey.from_text(subject, field_name="subject"), + predicate=CanonicalKey.from_text(predicate, field_name="predicate"), + value=value, + status=status, + confidence=confidence, + evidence_digests=evidence, + derived_from=tuple( + sorted( + { + CanonicalKey.from_text(item, field_name="derived_from") + for item in derived_from + }, + key=lambda item: item.value, + ) + ), + ) + + def key(self) -> tuple[str, str]: + """Return the subject-predicate state key.""" + return (self.subject.value, self.predicate.value) + + def to_payload(self) -> JsonObject: + """Return a canonical fact payload.""" + evidence: JsonArray = [ + {"algorithm": digest.algorithm, "value": digest.value} + for digest in self.evidence_digests + ] + derived: JsonArray = [item.value for item in self.derived_from] + return { + "fact_id": self.fact_id.value, + "subject": self.subject.value, + "predicate": self.predicate.value, + "value": self.value.to_payload(), + "status": self.status.value, + "confidence": self.confidence, + "evidence_digests": evidence, + "derived_from": derived, + } + + def digest(self) -> DigestRecord: + """Return a deterministic fact identity.""" + return DigestRecord.from_payload(self.to_payload()) + + +@dataclass(frozen=True, slots=True) +class FactPattern: + """One exact match condition used by causal and planning rules.""" + + subject: CanonicalKey + predicate: CanonicalKey + value: CognitiveValue + + @classmethod + def create( + cls, + *, + subject: str, + predicate: str, + value: CognitiveValue, + ) -> FactPattern: + """Create one canonical fact pattern.""" + return cls( + subject=CanonicalKey.from_text(subject, field_name="subject"), + predicate=CanonicalKey.from_text(predicate, field_name="predicate"), + value=value, + ) + + def matches(self, facts: Mapping[tuple[str, str], WorldFact]) -> bool: + """Return whether the current state exactly satisfies this pattern.""" + fact = facts.get((self.subject.value, self.predicate.value)) + return ( + fact is not None + and fact.value == self.value + and fact.status is not FactStatus.CONTRADICTED + ) + + def to_payload(self) -> JsonObject: + """Return a canonical pattern payload.""" + return { + "subject": self.subject.value, + "predicate": self.predicate.value, + "value": self.value.to_payload(), + } + + +@dataclass(frozen=True, slots=True) +class CausalRule: + """One transparent deterministic implication over exact fact patterns.""" + + rule_id: CanonicalKey + conditions: tuple[FactPattern, ...] + effect_subject: CanonicalKey + effect_predicate: CanonicalKey + effect_value: CognitiveValue + confidence: float + evidence_digests: tuple[DigestRecord, ...] + + @classmethod + def create( + cls, + *, + rule_id: str, + conditions: Iterable[FactPattern], + effect_subject: str, + effect_predicate: str, + effect_value: CognitiveValue, + confidence: float, + evidence_digests: Iterable[DigestRecord], + ) -> CausalRule: + """Create a causal rule that is evidence-bound and non-empty.""" + normalized_conditions = tuple(conditions) + evidence = tuple(evidence_digests) + if not normalized_conditions: + raise FoundationError("causal rule requires at least one condition") + if not evidence: + raise FoundationError("causal rule requires supporting evidence") + if not 0.0 <= confidence <= 1.0: + raise FoundationError("causal rule confidence must be between 0 and 1") + for digest in evidence: + digest.require_algorithm("sha256") + return cls( + rule_id=CanonicalKey.from_text(rule_id, field_name="rule_id"), + conditions=normalized_conditions, + effect_subject=CanonicalKey.from_text( + effect_subject, + field_name="effect_subject", + ), + effect_predicate=CanonicalKey.from_text( + effect_predicate, + field_name="effect_predicate", + ), + effect_value=effect_value, + confidence=confidence, + evidence_digests=evidence, + ) + + def to_payload(self) -> JsonObject: + """Return a canonical causal-rule payload.""" + conditions: JsonArray = [condition.to_payload() for condition in self.conditions] + evidence: JsonArray = [ + {"algorithm": digest.algorithm, "value": digest.value} + for digest in self.evidence_digests + ] + return { + "rule_id": self.rule_id.value, + "conditions": conditions, + "effect_subject": self.effect_subject.value, + "effect_predicate": self.effect_predicate.value, + "effect_value": self.effect_value.to_payload(), + "confidence": self.confidence, + "evidence_digests": evidence, + } + + def digest(self) -> DigestRecord: + """Return a deterministic rule identity.""" + return DigestRecord.from_payload(self.to_payload()) + + +@dataclass(frozen=True, slots=True) +class WorldModel: + """Immutable fact state and causal model with deterministic inference.""" + + facts: tuple[WorldFact, ...] = () + rules: tuple[CausalRule, ...] = () + + def __post_init__(self) -> None: + """Reject duplicate fact and rule identifiers.""" + fact_ids = [fact.fact_id.value for fact in self.facts] + rule_ids = [rule.rule_id.value for rule in self.rules] + if len(fact_ids) != len(set(fact_ids)): + raise FoundationError("world model contains duplicate fact identifiers") + if len(rule_ids) != len(set(rule_ids)): + raise FoundationError("world model contains duplicate rule identifiers") + + def state(self) -> dict[tuple[str, str], WorldFact]: + """Return the latest fact per subject-predicate key by tuple order.""" + state: dict[tuple[str, str], WorldFact] = {} + for fact in self.facts: + state[fact.key()] = fact + return state + + def observe(self, fact: WorldFact) -> WorldModel: + """Append one fact while requiring a unique identity.""" + if any(existing.fact_id == fact.fact_id for existing in self.facts): + raise FoundationError(f"world fact already exists: {fact.fact_id.value}") + return WorldModel((*self.facts, fact), self.rules) + + def add_rule(self, rule: CausalRule) -> WorldModel: + """Append one causal rule while requiring a unique identity.""" + if any(existing.rule_id == rule.rule_id for existing in self.rules): + raise FoundationError(f"causal rule already exists: {rule.rule_id.value}") + return WorldModel(self.facts, (*self.rules, rule)) + + def infer(self) -> WorldModel: + """Apply all currently satisfied rules exactly once in stable order.""" + model = self + state = model.state() + known_effects = { + (fact.subject.value, fact.predicate.value, fact.value.to_payload()["value"]) + for fact in model.facts + } + for rule in sorted(model.rules, key=lambda item: item.rule_id.value): + if not all(condition.matches(state) for condition in rule.conditions): + continue + effect_key = ( + rule.effect_subject.value, + rule.effect_predicate.value, + rule.effect_value.to_payload()["value"], + ) + if effect_key in known_effects: + continue + derived_ids = tuple( + state[(condition.subject.value, condition.predicate.value)].fact_id.value + for condition in rule.conditions + ) + inferred = WorldFact.create( + fact_id=f"inferred-{rule.rule_id.value}-{len(model.facts)}", + subject=rule.effect_subject.value, + predicate=rule.effect_predicate.value, + value=rule.effect_value, + status=FactStatus.INFERRED, + confidence=rule.confidence, + evidence_digests=(rule.digest(),), + derived_from=derived_ids, + ) + model = model.observe(inferred) + state = model.state() + known_effects.add(effect_key) + return model + + def predict(self) -> tuple[WorldFact, ...]: + """Return rule effects as predictions without mutating the model.""" + state = self.state() + predictions: list[WorldFact] = [] + for rule in sorted(self.rules, key=lambda item: item.rule_id.value): + if all(condition.matches(state) for condition in rule.conditions): + predictions.append( + WorldFact.create( + fact_id=f"prediction-{rule.rule_id.value}", + subject=rule.effect_subject.value, + predicate=rule.effect_predicate.value, + value=rule.effect_value, + status=FactStatus.PREDICTED, + confidence=rule.confidence, + evidence_digests=(rule.digest(),), + derived_from=tuple( + state[ + (condition.subject.value, condition.predicate.value) + ].fact_id.value + for condition in rule.conditions + ), + ) + ) + return tuple(predictions) + + def counterfactual(self, assumptions: Iterable[WorldFact]) -> WorldModel: + """Evaluate hypothetical assumptions in an isolated model branch.""" + model = self + for assumption in assumptions: + if assumption.status is not FactStatus.HYPOTHETICAL: + raise FoundationError("counterfactual assumptions must be hypothetical") + model = model.observe(assumption) + return model.infer() + + def to_payload(self) -> JsonObject: + """Return a canonical world-model payload.""" + facts: JsonArray = [ + fact.to_payload() + for fact in sorted( + self.facts, + key=lambda candidate: candidate.fact_id.value, + ) + ] + rules: JsonArray = [ + rule.to_payload() + for rule in sorted( + self.rules, + key=lambda candidate: candidate.rule_id.value, + ) + ] + counts: dict[str, JsonValue] = { + status.value: sum(1 for fact in self.facts if fact.status is status) + for status in FactStatus + } + return { + "fact_count": len(self.facts), + "rule_count": len(self.rules), + "status_counts": counts, + "facts": facts, + "rules": rules, + } + + def digest(self) -> DigestRecord: + """Return a deterministic world-model identity.""" + return DigestRecord.from_payload(self.to_payload()) diff --git a/src/ix_sally/contracts.py b/src/ix_sally/contracts.py index 5fec74d..9f05fa9 100644 --- a/src/ix_sally/contracts.py +++ b/src/ix_sally/contracts.py @@ -2,9 +2,9 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass, field from enum import StrEnum -from typing import Iterable from ix_sally.digest import DigestRecord, JsonArray, JsonObject from ix_sally.foundation import CanonicalKey, FoundationError, require_text diff --git a/src/ix_sally/cycles.py b/src/ix_sally/cycles.py index 7bf42a3..c988b79 100644 --- a/src/ix_sally/cycles.py +++ b/src/ix_sally/cycles.py @@ -2,9 +2,9 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass from enum import StrEnum -from typing import Iterable from ix_sally.agents import AgentRole from ix_sally.artifacts import AgentArtifact @@ -218,9 +218,7 @@ def _payload_reports_termination(payload: JsonObject) -> bool: def _payload_reports_human_review(payload: JsonObject) -> bool: """Return whether artifact payload data reports human-review need.""" - return _is_true(payload.get("requires_human_review")) or _is_true( - payload.get("has_blocker") - ) + return _is_true(payload.get("requires_human_review")) or _is_true(payload.get("has_blocker")) def _is_true(value: JsonValue | None) -> bool: diff --git a/src/ix_sally/dockets.py b/src/ix_sally/dockets.py index d772361..16d07bb 100644 --- a/src/ix_sally/dockets.py +++ b/src/ix_sally/dockets.py @@ -2,9 +2,9 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass from enum import StrEnum -from typing import Iterable from ix_sally.agents import AgentRole from ix_sally.artifacts import AgentArtifact, AgentArtifactKind @@ -170,9 +170,7 @@ def has_blocker(self) -> bool: def referenced_digests(self) -> tuple[DigestRecord, ...]: """Return all digests referenced by docket entries.""" return tuple( - entry.reference_digest - for entry in self.entries - if entry.reference_digest is not None + entry.reference_digest for entry in self.entries if entry.reference_digest is not None ) def to_artifact(self) -> AgentArtifact: diff --git a/src/ix_sally/doctrine.py b/src/ix_sally/doctrine.py index b932f07..61e71e0 100644 --- a/src/ix_sally/doctrine.py +++ b/src/ix_sally/doctrine.py @@ -2,9 +2,9 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass from enum import StrEnum -from typing import Iterable from ix_sally.digest import DigestRecord, JsonObject from ix_sally.foundation import CanonicalKey, FoundationError, require_text diff --git a/src/ix_sally/events.py b/src/ix_sally/events.py index 17357ec..e2ad354 100644 --- a/src/ix_sally/events.py +++ b/src/ix_sally/events.py @@ -135,7 +135,11 @@ def digest(self) -> DigestRecord: return DigestRecord.from_payload(self.to_payload()) -def event_payload_with_reference(*, reference_type: str, reference_digest: DigestRecord) -> JsonObject: +def event_payload_with_reference( + *, + reference_type: str, + reference_digest: DigestRecord, +) -> JsonObject: """Return a payload that links an event to another signed runtime record.""" key = CanonicalKey.from_text(reference_type, field_name="reference_type") reference_digest.require_algorithm("sha256") diff --git a/src/ix_sally/evidence.py b/src/ix_sally/evidence.py index 34c8a41..9472cad 100644 --- a/src/ix_sally/evidence.py +++ b/src/ix_sally/evidence.py @@ -2,9 +2,9 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass, field from enum import StrEnum -from typing import Iterable from ix_sally.agents import AgentRole from ix_sally.digest import DigestRecord, JsonArray, JsonObject diff --git a/src/ix_sally/evidence_support.py b/src/ix_sally/evidence_support.py index 82c8bd4..6fc26d4 100644 --- a/src/ix_sally/evidence_support.py +++ b/src/ix_sally/evidence_support.py @@ -2,9 +2,9 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass from enum import StrEnum -from typing import Iterable from ix_sally.agents import AgentRole from ix_sally.claims import ClaimRecord @@ -67,7 +67,9 @@ def create( raise FoundationError("evidence support findings must be reviewed by IX-Verity") if status is EvidenceSupportStatus.CONTRADICTED and normalized_contradiction is None: - raise FoundationError("contradicted evidence support findings require a contradiction note") + raise FoundationError( + "contradicted evidence support findings require a contradiction note" + ) if status is EvidenceSupportStatus.SUPPORTED and not normalized_evidence: raise FoundationError("supported evidence support findings require evidence digests") @@ -145,7 +147,9 @@ def create(cls, findings: Iterable[EvidenceSupportFinding]) -> EvidenceSupportLe for finding in normalized: if finding.finding_id.value in seen: - raise FoundationError(f"duplicate evidence support finding id: {finding.finding_id.value}") + raise FoundationError( + f"duplicate evidence support finding id: {finding.finding_id.value}" + ) seen.add(finding.finding_id.value) return cls(findings=normalized) @@ -227,7 +231,10 @@ def review_claim( status=EvidenceSupportStatus.CONTRADICTED, rationale="Recorded evidence contains contradiction language for claim terms.", evidence_digests=tuple(evidence.digest() for evidence in contradicted), - contradiction_note="Evidence contains failed, blocked, denied, contradiction, or unsupported language.", + contradiction_note=( + "Evidence contains failed, blocked, denied, contradiction, " + "or unsupported language." + ), ) supporting = self._supporting_evidence( @@ -259,8 +266,7 @@ def review_claims( ) -> EvidenceSupportLedger: """Review multiple claims in ledger order.""" return EvidenceSupportLedger.create( - self.review_claim(claim=claim, evidence_records=evidence_records) - for claim in claims + self.review_claim(claim=claim, evidence_records=evidence_records) for claim in claims ) def _claim_terms(self, statement: str) -> tuple[str, ...]: diff --git a/src/ix_sally/evidence_support_processing.py b/src/ix_sally/evidence_support_processing.py index 4b2391f..8d852bd 100644 --- a/src/ix_sally/evidence_support_processing.py +++ b/src/ix_sally/evidence_support_processing.py @@ -2,7 +2,7 @@ from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, field from ix_sally.claims import ClaimRecord from ix_sally.digest import DigestRecord, JsonArray, JsonObject @@ -53,11 +53,7 @@ def supported_count(self) -> int: def human_review_count(self) -> int: """Return how many reviewed claims require human review.""" - return sum( - 1 - for result in self.processed - if result.finding.requires_human_review() - ) + return sum(1 for result in self.processed if result.finding.requires_human_review()) def to_payload(self) -> JsonObject: """Return a stable JSON-compatible support batch result.""" @@ -83,7 +79,7 @@ class EvidenceSupportProcessor: """Runs IX-Verity support reviews and records findings into run state.""" recorder: StateRecorder - reviewer: VerityEvidenceSupportReview = VerityEvidenceSupportReview() + reviewer: VerityEvidenceSupportReview = field(default_factory=VerityEvidenceSupportReview) def process_claim( self, @@ -148,6 +144,5 @@ def _claim_already_reviewed( """Return whether a support finding already references this claim digest.""" claim_digest = claim.digest() return any( - finding.claim_digest == claim_digest - for finding in state.evidence_support.findings + finding.claim_digest == claim_digest for finding in state.evidence_support.findings ) diff --git a/src/ix_sally/execution_queue.py b/src/ix_sally/execution_queue.py index 9cc2409..75f6c21 100644 --- a/src/ix_sally/execution_queue.py +++ b/src/ix_sally/execution_queue.py @@ -2,9 +2,9 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass from enum import StrEnum -from typing import Iterable from ix_sally.actions import BoundedActionLedger, BoundedActionRecord from ix_sally.agents import AgentRole @@ -168,7 +168,9 @@ def create(cls, items: Iterable[ExecutionQueueItem]) -> ExecutionQueue: @classmethod def from_action_ledger(cls, ledger: BoundedActionLedger) -> ExecutionQueue: """Queue every action currently authorized for execution.""" - return cls.create(ExecutionQueueItem.from_action(action) for action in ledger.executable_actions()) + return cls.create( + ExecutionQueueItem.from_action(action) for action in ledger.executable_actions() + ) def append(self, item: ExecutionQueueItem) -> ExecutionQueue: """Return a new queue with an appended item.""" diff --git a/src/ix_sally/executions.py b/src/ix_sally/executions.py index 3cafc73..5009beb 100644 --- a/src/ix_sally/executions.py +++ b/src/ix_sally/executions.py @@ -2,9 +2,9 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass from enum import StrEnum -from typing import Iterable from ix_sally.agents import AgentRole from ix_sally.artifacts import AgentArtifact, AgentArtifactKind @@ -84,9 +84,11 @@ def create( if status is ExecutionStatus.FAILED and exit_code in {None, 0}: raise FoundationError("failed execution receipts require a non-zero exit_code") - if status in {ExecutionStatus.BLOCKED, ExecutionStatus.TIMED_OUT}: - if normalized_boundary_note is None: - raise FoundationError("blocked or timed-out executions require a boundary note") + if ( + status in {ExecutionStatus.BLOCKED, ExecutionStatus.TIMED_OUT} + and normalized_boundary_note is None + ): + raise FoundationError("blocked or timed-out executions require a boundary note") return cls( receipt_id=receipt_id diff --git a/src/ix_sally/falsifications.py b/src/ix_sally/falsifications.py index 95bc347..7cd8054 100644 --- a/src/ix_sally/falsifications.py +++ b/src/ix_sally/falsifications.py @@ -2,9 +2,9 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass from enum import StrEnum -from typing import Iterable from ix_sally.agents import AgentRole from ix_sally.artifacts import AgentArtifact, AgentArtifactKind diff --git a/src/ix_sally/forge_evidence.py b/src/ix_sally/forge_evidence.py index ed85e9b..40b2104 100644 --- a/src/ix_sally/forge_evidence.py +++ b/src/ix_sally/forge_evidence.py @@ -155,13 +155,22 @@ def record_all_result_evidence( def _summary_for_result(self, result: ForgeResultRecord) -> str: """Return a receipt-grade evidence summary for a Forge result.""" if result.observed_output is not None: - return f"Forge result {result.status.value}: {result.summary} Output: {result.observed_output}" + return ( + f"Forge result {result.status.value}: {result.summary} " + f"Output: {result.observed_output}" + ) if result.failure_reason is not None: - return f"Forge result {result.status.value}: {result.summary} Reason: {result.failure_reason}" + return ( + f"Forge result {result.status.value}: {result.summary} " + f"Reason: {result.failure_reason}" + ) if result.boundary_note is not None: - return f"Forge result {result.status.value}: {result.summary} Boundary: {result.boundary_note}" + return ( + f"Forge result {result.status.value}: {result.summary} " + f"Boundary: {result.boundary_note}" + ) return f"Forge result {result.status.value}: {result.summary}" diff --git a/src/ix_sally/forge_result_gateway.py b/src/ix_sally/forge_result_gateway.py index 14a6855..078f83a 100644 --- a/src/ix_sally/forge_result_gateway.py +++ b/src/ix_sally/forge_result_gateway.py @@ -2,8 +2,8 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass -from typing import Iterable from ix_sally.digest import DigestRecord, JsonArray, JsonObject from ix_sally.forge_result_processing import ( @@ -50,15 +50,11 @@ def create( if passed_count < 0: raise FoundationError("Forge result submission passed_count must not be negative") if human_review_count < 0: - raise FoundationError( - "Forge result submission human_review_count must not be negative" - ) + raise FoundationError("Forge result submission human_review_count must not be negative") if passed_count > result_count: raise FoundationError("Forge result submission passed_count exceeds result_count") if human_review_count > result_count: - raise FoundationError( - "Forge result submission human_review_count exceeds result_count" - ) + raise FoundationError("Forge result submission human_review_count exceeds result_count") before_state_digest.require_algorithm("sha256") after_state_digest.require_algorithm("sha256") @@ -256,9 +252,7 @@ def _require_results_match_pending_dispatches( if queue_digest in recorded_queue_digests: raise FoundationError("Forge result already recorded for dispatched queue item") if queue_digest not in pending_queue_digests: - raise FoundationError( - "Forge result does not match a pending dispatched queue item" - ) + raise FoundationError("Forge result does not match a pending dispatched queue item") action = state.actions.require_action(result.action_id.value) if result.action_digest != action.digest(): diff --git a/src/ix_sally/forge_results.py b/src/ix_sally/forge_results.py index 7d5893b..cc064e0 100644 --- a/src/ix_sally/forge_results.py +++ b/src/ix_sally/forge_results.py @@ -2,9 +2,9 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass from enum import StrEnum -from typing import Iterable from ix_sally.actions import BoundedActionRecord from ix_sally.agents import AgentRole diff --git a/src/ix_sally/human_review_audited_reentry.py b/src/ix_sally/human_review_audited_reentry.py index 7d04dfd..2ff12e0 100644 --- a/src/ix_sally/human_review_audited_reentry.py +++ b/src/ix_sally/human_review_audited_reentry.py @@ -7,19 +7,17 @@ from ix_sally.digest import DigestRecord, JsonObject from ix_sally.foundation import CanonicalKey, FoundationError from ix_sally.human_review_control_plane import HumanReviewControlPlaneState -from ix_sally.human_review_control_plane_report import ( - HumanReviewControlPlaneReportStatus, -) -from ix_sally.human_review_reentry import HumanReviewReentryStatus +from ix_sally.human_review_control_plane_report_status import HumanReviewControlPlaneReportStatus from ix_sally.human_review_reentry_audit import ( - HumanReviewReentryAuditReport, - HumanReviewReentryAuditStatus, HumanReviewReentryAuditor, + HumanReviewReentryAuditReport, ) +from ix_sally.human_review_reentry_audit_status import HumanReviewReentryAuditStatus from ix_sally.human_review_reentry_coordination import ( HumanReviewReentryCoordinationResult, HumanReviewReentryCoordinator, ) +from ix_sally.human_review_reentry_status import HumanReviewReentryStatus from ix_sally.human_review_workflow import ( HumanReviewWorkflowKit, HumanReviewWorkflowOperation, @@ -79,9 +77,7 @@ def create( "audited human-review reentry executed_steps must not be negative" ) if executed_steps > max_steps: - raise FoundationError( - "audited human-review reentry executed_steps exceeds max_steps" - ) + raise FoundationError("audited human-review reentry executed_steps exceeds max_steps") resume_operation_digest.require_algorithm("sha256") reentry_coordination_digest.require_algorithm("sha256") @@ -140,10 +136,7 @@ def audit_failed(self) -> bool: def waiting_for_external_input(self) -> bool: """Return whether the audited reentry is valid but waiting externally.""" - return ( - self.audit_status - is HumanReviewReentryAuditStatus.WAITING_FOR_EXTERNAL_INPUT - ) + return self.audit_status is HumanReviewReentryAuditStatus.WAITING_FOR_EXTERNAL_INPUT def to_payload(self) -> JsonObject: """Return a stable JSON-compatible audited reentry receipt.""" @@ -226,9 +219,7 @@ def create( ) -> AuditedHumanReviewReentryResult: """Create a normalized audited reentry result and validate all links.""" if resume_operation.receipt.workflow_stage is not HumanReviewWorkflowStage.RESUME_RECORDED: - raise FoundationError( - "audited human-review reentry requires resume-recorded operation" - ) + raise FoundationError("audited human-review reentry requires resume-recorded operation") if reentry_coordination.resume_operation.digest() != resume_operation.digest(): raise FoundationError("audited human-review reentry resume mismatch") if audit_report.coordination_digest != reentry_coordination.digest(): @@ -237,39 +228,26 @@ def create( audit_workflow_operation.receipt.workflow_stage is not HumanReviewWorkflowStage.REENTRY_AUDIT_RECORDED ): - raise FoundationError( - "audited human-review reentry requires audit-recorded workflow" - ) + raise FoundationError("audited human-review reentry requires audit-recorded workflow") if ( audit_workflow_operation.require_reentry_audit_report().digest() != audit_report.digest() ): - raise FoundationError( - "audited human-review reentry workflow audit mismatch" - ) + raise FoundationError("audited human-review reentry workflow audit mismatch") if audit_workflow_operation.run_state.digest() != audit_report.state_digest: raise FoundationError("audited human-review reentry state mismatch") if receipt.resume_operation_digest != resume_operation.digest(): raise FoundationError("audited human-review reentry receipt resume mismatch") if receipt.reentry_coordination_digest != reentry_coordination.digest(): - raise FoundationError( - "audited human-review reentry receipt coordination mismatch" - ) + raise FoundationError("audited human-review reentry receipt coordination mismatch") if receipt.audit_report_digest != audit_report.digest(): raise FoundationError("audited human-review reentry receipt audit mismatch") if receipt.audit_workflow_operation_digest != audit_workflow_operation.digest(): - raise FoundationError( - "audited human-review reentry receipt workflow mismatch" - ) + raise FoundationError("audited human-review reentry receipt workflow mismatch") if receipt.after_state_digest != audit_workflow_operation.run_state.digest(): raise FoundationError("audited human-review reentry receipt state mismatch") - if ( - receipt.after_control_plane_digest - != audit_workflow_operation.control_plane.digest() - ): - raise FoundationError( - "audited human-review reentry receipt control-plane mismatch" - ) + if receipt.after_control_plane_digest != audit_workflow_operation.control_plane.digest(): + raise FoundationError("audited human-review reentry receipt control-plane mismatch") return cls( resume_operation=resume_operation, @@ -334,9 +312,7 @@ def to_payload(self) -> JsonObject: "resume_operation_digest": self.resume_operation.digest().value, "reentry_coordination_digest": self.reentry_coordination.digest().value, "audit_report_digest": self.audit_report.digest().value, - "audit_workflow_operation_digest": ( - self.audit_workflow_operation.digest().value - ), + "audit_workflow_operation_digest": (self.audit_workflow_operation.digest().value), "receipt_digest": self.receipt.digest().value, "state_digest": self.state.digest().value, "control_plane_digest": self.control_plane.digest().value, @@ -385,9 +361,7 @@ def resume_audit_and_record( ) -> AuditedHumanReviewReentryResult: """Run reentry, audit it, and record the audit in one operation.""" if resume_operation.receipt.workflow_stage is not HumanReviewWorkflowStage.RESUME_RECORDED: - raise FoundationError( - "audited human-review reentry requires resume-recorded operation" - ) + raise FoundationError("audited human-review reentry requires resume-recorded operation") reentry_coordination = self.reentry_coordinator.resume_and_record( resume_operation=resume_operation, diff --git a/src/ix_sally/human_review_audited_reentry_ledger.py b/src/ix_sally/human_review_audited_reentry_ledger.py index 1107ade..4082495 100644 --- a/src/ix_sally/human_review_audited_reentry_ledger.py +++ b/src/ix_sally/human_review_audited_reentry_ledger.py @@ -2,8 +2,9 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass -from typing import TYPE_CHECKING, Iterable +from typing import TYPE_CHECKING from ix_sally.digest import DigestRecord, JsonArray, JsonObject from ix_sally.foundation import CanonicalKey, FoundationError @@ -11,11 +12,11 @@ if TYPE_CHECKING: from ix_sally.human_review_audited_reentry import AuditedHumanReviewReentryResult - from ix_sally.human_review_control_plane_report import ( + from ix_sally.human_review_control_plane_report_status import ( HumanReviewControlPlaneReportStatus, ) - from ix_sally.human_review_reentry import HumanReviewReentryStatus - from ix_sally.human_review_reentry_audit import HumanReviewReentryAuditStatus + from ix_sally.human_review_reentry_audit_status import HumanReviewReentryAuditStatus + from ix_sally.human_review_reentry_status import HumanReviewReentryStatus @dataclass(frozen=True, slots=True) @@ -68,13 +69,9 @@ def create( ) -> AuditedHumanReviewReentryLedgerEntry: """Create a normalized audited human-review reentry ledger entry.""" if sequence <= 0: - raise FoundationError( - "audited human-review reentry ledger sequence must be positive" - ) + raise FoundationError("audited human-review reentry ledger sequence must be positive") if max_steps <= 0: - raise FoundationError( - "audited human-review reentry ledger max_steps must be positive" - ) + raise FoundationError("audited human-review reentry ledger max_steps must be positive") if executed_steps < 0: raise FoundationError( "audited human-review reentry ledger executed_steps must not be negative" @@ -138,9 +135,7 @@ def from_result( resume_operation_digest=result.receipt.resume_operation_digest, reentry_coordination_digest=result.receipt.reentry_coordination_digest, audit_report_digest=result.receipt.audit_report_digest, - audit_workflow_operation_digest=( - result.receipt.audit_workflow_operation_digest - ), + audit_workflow_operation_digest=(result.receipt.audit_workflow_operation_digest), before_state_digest=result.receipt.before_state_digest, after_state_digest=result.receipt.after_state_digest, before_control_plane_digest=result.receipt.before_control_plane_digest, @@ -304,8 +299,7 @@ def create( for entry in normalized: if entry.sequence in seen_sequences: raise FoundationError( - f"duplicate audited human-review reentry ledger sequence: " - f"{entry.sequence}" + f"duplicate audited human-review reentry ledger sequence: {entry.sequence}" ) if entry.entry_id.value in seen_entry_ids: raise FoundationError( @@ -318,9 +312,7 @@ def create( f"{entry.audited_reentry_result_digest.value}" ) if entry.sequence <= previous_sequence: - raise FoundationError( - "audited human-review reentry ledger sequences must increase" - ) + raise FoundationError("audited human-review reentry ledger sequences must increase") seen_sequences.add(entry.sequence) seen_entry_ids.add(entry.entry_id.value) @@ -428,18 +420,12 @@ def to_payload(self) -> JsonObject: "waiting_entry_count": len(self.waiting_entries()), "operator_attention_entry_count": len(self.operator_attention_entries()), "changed_state_entry_count": len(self.changed_state_entries()), - "forge_dispatch_entry_count": len( - self.entries_for_stage(RunStage.FORGE_DISPATCH) - ), + "forge_dispatch_entry_count": len(self.entries_for_stage(RunStage.FORGE_DISPATCH)), "forge_result_processing_entry_count": len( self.entries_for_stage(RunStage.FORGE_RESULT_PROCESSING) ), "passed_audit_entry_count": len( - [ - entry - for entry in self.entries - if entry.audit_status.value == "passed" - ] + [entry for entry in self.entries if entry.audit_status.value == "passed"] ), "waiting_audit_entry_count": len( [ @@ -449,11 +435,7 @@ def to_payload(self) -> JsonObject: ] ), "advanced_reentry_entry_count": len( - [ - entry - for entry in self.entries - if entry.reentry_status.value == "advanced" - ] + [entry for entry in self.entries if entry.reentry_status.value == "advanced"] ), "waiting_reentry_entry_count": len( [ diff --git a/src/ix_sally/human_review_bundle.py b/src/ix_sally/human_review_bundle.py index a845fc7..90f9114 100644 --- a/src/ix_sally/human_review_bundle.py +++ b/src/ix_sally/human_review_bundle.py @@ -59,9 +59,7 @@ def create( ) surfaced_count = ( - gateway_resolvable_count - + manual_investigation_count - + blocker_acknowledgment_count + gateway_resolvable_count + manual_investigation_count + blocker_acknowledgment_count ) if surfaced_count != target_count: raise FoundationError( @@ -252,16 +250,14 @@ def assemble( *, state: NinefoldRunState, authority_note: str = ( - "Human authority is required before IX-Sally may treat these " - "targets as resolved." + "Human authority is required before IX-Sally may treat these targets as resolved." ), ) -> HumanReviewOperatorBundle: """Assemble a docket, packet, and receipt for the active human-review stage.""" snapshot = RunStageSnapshot.from_state(state) if snapshot.stage is not RunStage.HUMAN_REVIEW: raise FoundationError( - f"human-review bundle expected human_review but observed " - f"{snapshot.stage.value}" + f"human-review bundle expected human_review but observed {snapshot.stage.value}" ) docket = self.docket_builder.build(state=state) diff --git a/src/ix_sally/human_review_bundle_ledger.py b/src/ix_sally/human_review_bundle_ledger.py index 5274a3b..5c4d5f8 100644 --- a/src/ix_sally/human_review_bundle_ledger.py +++ b/src/ix_sally/human_review_bundle_ledger.py @@ -2,8 +2,8 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass -from typing import Iterable from ix_sally.digest import DigestRecord, JsonArray, JsonObject from ix_sally.foundation import CanonicalKey, FoundationError, require_text @@ -66,9 +66,7 @@ def create( ) surfaced_count = ( - gateway_resolvable_count - + manual_investigation_count - + blocker_acknowledgment_count + gateway_resolvable_count + manual_investigation_count + blocker_acknowledgment_count ) if surfaced_count != target_count: raise FoundationError( @@ -216,17 +214,14 @@ def create( ) if entry.entry_id.value in seen_entry_ids: raise FoundationError( - f"duplicate human-review bundle ledger entry id: " - f"{entry.entry_id.value}" + f"duplicate human-review bundle ledger entry id: {entry.entry_id.value}" ) if entry.bundle_digest.value in seen_bundle_digests: raise FoundationError( f"duplicate human-review bundle digest: {entry.bundle_digest.value}" ) if entry.sequence <= previous_sequence: - raise FoundationError( - "human-review bundle ledger sequences must increase" - ) + raise FoundationError("human-review bundle ledger sequences must increase") seen_sequences.add(entry.sequence) seen_entry_ids.add(entry.entry_id.value) @@ -270,9 +265,7 @@ def manual_investigation_entries(self) -> tuple[HumanReviewBundleLedgerEntry, .. def blocker_acknowledgment_entries(self) -> tuple[HumanReviewBundleLedgerEntry, ...]: """Return entries that include blocker-only cards.""" - return tuple( - entry for entry in self.entries if entry.has_blocker_acknowledgment_cards() - ) + return tuple(entry for entry in self.entries if entry.has_blocker_acknowledgment_cards()) def to_payload(self) -> JsonObject: """Return a stable JSON-compatible human-review bundle ledger.""" @@ -289,9 +282,7 @@ def to_payload(self) -> JsonObject: "latest_entry_digest": latest.digest().value if latest is not None else None, "gateway_resolvable_entry_count": len(self.gateway_resolvable_entries()), "manual_investigation_entry_count": len(self.manual_investigation_entries()), - "blocker_acknowledgment_entry_count": len( - self.blocker_acknowledgment_entries() - ), + "blocker_acknowledgment_entry_count": len(self.blocker_acknowledgment_entries()), } def digest(self) -> DigestRecord: diff --git a/src/ix_sally/human_review_clearance.py b/src/ix_sally/human_review_clearance.py index 6679189..2fecbf9 100644 --- a/src/ix_sally/human_review_clearance.py +++ b/src/ix_sally/human_review_clearance.py @@ -77,9 +77,7 @@ def create( "blocker_acknowledgment_count": blocker_acknowledgment_count, }.items(): if value < 0: - raise FoundationError( - f"human-review clearance {field_name} must not be negative" - ) + raise FoundationError(f"human-review clearance {field_name} must not be negative") surfaced_count = ( pending_decision_count @@ -88,14 +86,10 @@ def create( + blocker_acknowledgment_count ) if surfaced_count != card_count: - raise FoundationError( - "human-review clearance surfaced counts must equal card_count" - ) + raise FoundationError("human-review clearance surfaced counts must equal card_count") decision_status_count = ( - approved_decision_count - + rejected_decision_count - + deferred_decision_count + approved_decision_count + rejected_decision_count + deferred_decision_count ) if decision_status_count != resolved_count: raise FoundationError( @@ -249,19 +243,13 @@ def create( if resolution_audit.bundle_digest != bundle.digest(): raise FoundationError("human-review clearance audit does not match bundle") if resolution_audit.decision_ledger_digest != decision_ledger.digest(): - raise FoundationError( - "human-review clearance audit does not match decision ledger" - ) + raise FoundationError("human-review clearance audit does not match decision ledger") if clearance_report.bundle_digest != bundle.digest(): raise FoundationError("human-review clearance report does not match bundle") if clearance_report.decision_ledger_digest != decision_ledger.digest(): - raise FoundationError( - "human-review clearance report does not match decision ledger" - ) + raise FoundationError("human-review clearance report does not match decision ledger") if clearance_report.resolution_audit_digest != resolution_audit.digest(): - raise FoundationError( - "human-review clearance report does not match resolution audit" - ) + raise FoundationError("human-review clearance report does not match resolution audit") return cls( bundle=bundle, diff --git a/src/ix_sally/human_review_complete_reentry.py b/src/ix_sally/human_review_complete_reentry.py index 5134138..6b75e49 100644 --- a/src/ix_sally/human_review_complete_reentry.py +++ b/src/ix_sally/human_review_complete_reentry.py @@ -11,11 +11,9 @@ AuditedHumanReviewReentryResult, ) from ix_sally.human_review_control_plane import HumanReviewControlPlaneState -from ix_sally.human_review_control_plane_report import ( - HumanReviewControlPlaneReportStatus, -) -from ix_sally.human_review_reentry import HumanReviewReentryStatus -from ix_sally.human_review_reentry_audit import HumanReviewReentryAuditStatus +from ix_sally.human_review_control_plane_report_status import HumanReviewControlPlaneReportStatus +from ix_sally.human_review_reentry_audit_status import HumanReviewReentryAuditStatus +from ix_sally.human_review_reentry_status import HumanReviewReentryStatus from ix_sally.human_review_workflow import ( HumanReviewWorkflowKit, HumanReviewWorkflowOperation, @@ -69,17 +67,13 @@ def create( ) -> CompleteHumanReviewReentryReceipt: """Create a normalized complete human-review reentry receipt.""" if max_steps <= 0: - raise FoundationError( - "complete human-review reentry max_steps must be positive" - ) + raise FoundationError("complete human-review reentry max_steps must be positive") if executed_steps < 0: raise FoundationError( "complete human-review reentry executed_steps must not be negative" ) if executed_steps > max_steps: - raise FoundationError( - "complete human-review reentry executed_steps exceeds max_steps" - ) + raise FoundationError("complete human-review reentry executed_steps exceeds max_steps") resume_operation_digest.require_algorithm("sha256") audited_reentry_result_digest.require_algorithm("sha256") @@ -198,9 +192,7 @@ def to_payload(self) -> JsonObject: "executed_steps": self.executed_steps, "changed_state": self.changed_state(), "recorded_reentry_and_audit": self.recorded_reentry_and_audit(), - "recorded_complete_audited_reentry": ( - self.recorded_complete_audited_reentry() - ), + "recorded_complete_audited_reentry": (self.recorded_complete_audited_reentry()), "accepted": self.accepted(), "requires_operator_attention": self.requires_operator_attention(), "waiting_for_external_input": self.waiting_for_external_input(), @@ -240,16 +232,12 @@ def create( final_workflow_operation.receipt.workflow_stage is not HumanReviewWorkflowStage.AUDITED_REENTRY_RECORDED ): - raise FoundationError( - "complete human-review reentry requires audited-reentry workflow" - ) + raise FoundationError("complete human-review reentry requires audited-reentry workflow") if ( final_workflow_operation.require_audited_reentry_result().digest() != audited_reentry_result.digest() ): - raise FoundationError( - "complete human-review reentry final workflow result mismatch" - ) + raise FoundationError("complete human-review reentry final workflow result mismatch") if final_workflow_operation.run_state.digest() != audited_reentry_result.state.digest(): raise FoundationError("complete human-review reentry state mismatch") if receipt.resume_operation_digest != resume_operation.digest(): @@ -257,19 +245,13 @@ def create( if receipt.audited_reentry_result_digest != audited_reentry_result.digest(): raise FoundationError("complete human-review reentry receipt result mismatch") if receipt.audited_reentry_receipt_digest != audited_reentry_result.receipt.digest(): - raise FoundationError( - "complete human-review reentry receipt audited receipt mismatch" - ) + raise FoundationError("complete human-review reentry receipt audited receipt mismatch") if receipt.final_workflow_operation_digest != final_workflow_operation.digest(): - raise FoundationError( - "complete human-review reentry receipt workflow mismatch" - ) + raise FoundationError("complete human-review reentry receipt workflow mismatch") if receipt.after_state_digest != final_workflow_operation.run_state.digest(): raise FoundationError("complete human-review reentry receipt state mismatch") if receipt.after_control_plane_digest != final_workflow_operation.control_plane.digest(): - raise FoundationError( - "complete human-review reentry receipt control-plane mismatch" - ) + raise FoundationError("complete human-review reentry receipt control-plane mismatch") return cls( resume_operation=resume_operation, @@ -321,9 +303,7 @@ def to_payload(self) -> JsonObject: return { "resume_operation_digest": self.resume_operation.digest().value, "audited_reentry_result_digest": self.audited_reentry_result.digest().value, - "final_workflow_operation_digest": ( - self.final_workflow_operation.digest().value - ), + "final_workflow_operation_digest": (self.final_workflow_operation.digest().value), "receipt_digest": self.receipt.digest().value, "state_digest": self.state.digest().value, "control_plane_digest": self.control_plane.digest().value, diff --git a/src/ix_sally/human_review_complete_reentry_closeout_coordination.py b/src/ix_sally/human_review_complete_reentry_closeout_coordination.py index 90c95c6..b9315b7 100644 --- a/src/ix_sally/human_review_complete_reentry_closeout_coordination.py +++ b/src/ix_sally/human_review_complete_reentry_closeout_coordination.py @@ -10,19 +10,17 @@ CompleteHumanReviewReentryCoordinator, CompleteHumanReviewReentryResult, ) -from ix_sally.human_review_complete_reentry_report import ( - CompleteHumanReviewReentryCloseoutReport, +from ix_sally.human_review_complete_reentry_closeout_status import ( CompleteHumanReviewReentryCloseoutStatus, ) +from ix_sally.human_review_complete_reentry_report import CompleteHumanReviewReentryCloseoutReport from ix_sally.human_review_control_plane import HumanReviewControlPlaneState from ix_sally.human_review_control_plane_coordinator import ( HumanReviewControlPlaneOperationKind, ) -from ix_sally.human_review_control_plane_report import ( - HumanReviewControlPlaneReportStatus, -) -from ix_sally.human_review_reentry import HumanReviewReentryStatus -from ix_sally.human_review_reentry_audit import HumanReviewReentryAuditStatus +from ix_sally.human_review_control_plane_report_status import HumanReviewControlPlaneReportStatus +from ix_sally.human_review_reentry_audit_status import HumanReviewReentryAuditStatus +from ix_sally.human_review_reentry_status import HumanReviewReentryStatus from ix_sally.human_review_workflow import ( HumanReviewWorkflowKit, HumanReviewWorkflowOperation, @@ -81,8 +79,7 @@ def create( """Create a normalized complete reentry closeout coordination receipt.""" if max_steps <= 0: raise FoundationError( - "complete human-review reentry closeout coordination max_steps " - "must be positive" + "complete human-review reentry closeout coordination max_steps must be positive" ) if executed_steps < 0: raise FoundationError( @@ -247,9 +244,7 @@ def create( closeout_workflow_operation: HumanReviewWorkflowOperation, ) -> CompleteHumanReviewReentryCloseoutCoordinationResult: """Create a validated complete reentry closeout coordination result.""" - if closeout_report.complete_reentry_result_digest != ( - complete_reentry_result.digest() - ): + if closeout_report.complete_reentry_result_digest != (complete_reentry_result.digest()): raise FoundationError( "complete reentry closeout report must match complete reentry result" ) @@ -262,9 +257,7 @@ def create( if closeout_workflow_operation.receipt.workflow_stage is not ( HumanReviewWorkflowStage.COMPLETE_REENTRY_CLOSEOUT_RECORDED ): - raise FoundationError( - "complete reentry closeout workflow must record closeout stage" - ) + raise FoundationError("complete reentry closeout workflow must record closeout stage") if closeout_workflow_operation.operation_kind() is not ( HumanReviewControlPlaneOperationKind.COMPLETE_REENTRY_CLOSEOUT_RECORDED ): @@ -275,9 +268,7 @@ def create( closeout_workflow_operation.require_complete_reentry_closeout_report() != closeout_report ): - raise FoundationError( - "complete reentry closeout workflow must carry closeout report" - ) + raise FoundationError("complete reentry closeout workflow must carry closeout report") if closeout_workflow_operation.run_state.digest() != ( complete_reentry_result.state.digest() ): @@ -286,9 +277,7 @@ def create( ) receipt = CompleteHumanReviewReentryCloseoutCoordinationReceipt.create( - resume_operation_digest=( - complete_reentry_result.receipt.resume_operation_digest - ), + resume_operation_digest=(complete_reentry_result.receipt.resume_operation_digest), complete_reentry_result_digest=complete_reentry_result.digest(), complete_reentry_receipt_digest=complete_reentry_result.receipt.digest(), closeout_report_digest=closeout_report.digest(), @@ -351,9 +340,7 @@ def to_payload(self) -> JsonObject: return { "complete_reentry_result_digest": self.complete_reentry_result.digest().value, "closeout_report_digest": self.closeout_report.digest().value, - "closeout_workflow_operation_digest": ( - self.closeout_workflow_operation.digest().value - ), + "closeout_workflow_operation_digest": (self.closeout_workflow_operation.digest().value), "receipt_digest": self.receipt.digest().value, "state_digest": self.state.digest().value, "control_plane_digest": self.control_plane.digest().value, @@ -396,15 +383,11 @@ def resume_closeout_and_record( max_steps: int, ) -> CompleteHumanReviewReentryCloseoutCoordinationResult: """Resume, audit, finalize, close out, and record closeout in one call.""" - complete_reentry = ( - self.complete_reentry_coordinator.resume_audit_record_and_finalize( - resume_operation=resume_operation, - max_steps=max_steps, - ) - ) - closeout_report = CompleteHumanReviewReentryCloseoutReport.from_result( - complete_reentry + complete_reentry = self.complete_reentry_coordinator.resume_audit_record_and_finalize( + resume_operation=resume_operation, + max_steps=max_steps, ) + closeout_report = CompleteHumanReviewReentryCloseoutReport.from_result(complete_reentry) closeout_workflow = self.workflow.record_complete_reentry_closeout( run_state=complete_reentry.state, closeout_report=closeout_report, diff --git a/src/ix_sally/human_review_complete_reentry_closeout_coordination_ledger.py b/src/ix_sally/human_review_complete_reentry_closeout_coordination_ledger.py index 705649e..c8a1391 100644 --- a/src/ix_sally/human_review_complete_reentry_closeout_coordination_ledger.py +++ b/src/ix_sally/human_review_complete_reentry_closeout_coordination_ledger.py @@ -2,12 +2,13 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass -from typing import TYPE_CHECKING, Iterable +from typing import TYPE_CHECKING from ix_sally.digest import DigestRecord, JsonArray, JsonObject from ix_sally.foundation import CanonicalKey, FoundationError -from ix_sally.human_review_complete_reentry_report import ( +from ix_sally.human_review_complete_reentry_closeout_status import ( CompleteHumanReviewReentryCloseoutStatus, ) from ix_sally.stage_readiness import RunStage @@ -16,11 +17,11 @@ from ix_sally.human_review_complete_reentry_closeout_coordination import ( CompleteHumanReviewReentryCloseoutCoordinationResult, ) - from ix_sally.human_review_control_plane_report import ( + from ix_sally.human_review_control_plane_report_status import ( HumanReviewControlPlaneReportStatus, ) - from ix_sally.human_review_reentry import HumanReviewReentryStatus - from ix_sally.human_review_reentry_audit import HumanReviewReentryAuditStatus + from ix_sally.human_review_reentry_audit_status import HumanReviewReentryAuditStatus + from ix_sally.human_review_reentry_status import HumanReviewReentryStatus @dataclass(frozen=True, slots=True) @@ -78,23 +79,19 @@ def create( """Create a normalized closeout coordination ledger entry.""" if sequence <= 0: raise FoundationError( - "complete reentry closeout coordination ledger sequence " - "must be positive" + "complete reentry closeout coordination ledger sequence must be positive" ) if max_steps <= 0: raise FoundationError( - "complete reentry closeout coordination ledger max_steps " - "must be positive" + "complete reentry closeout coordination ledger max_steps must be positive" ) if executed_steps < 0: raise FoundationError( - "complete reentry closeout coordination ledger executed_steps " - "must not be negative" + "complete reentry closeout coordination ledger executed_steps must not be negative" ) if executed_steps > max_steps: raise FoundationError( - "complete reentry closeout coordination ledger executed_steps " - "exceeds max_steps" + "complete reentry closeout coordination ledger executed_steps exceeds max_steps" ) coordination_result_digest.require_algorithm("sha256") @@ -153,16 +150,10 @@ def from_result( coordination_result_digest=result.digest(), coordination_receipt_digest=result.receipt.digest(), resume_operation_digest=result.receipt.resume_operation_digest, - complete_reentry_result_digest=( - result.receipt.complete_reentry_result_digest - ), - complete_reentry_receipt_digest=( - result.receipt.complete_reentry_receipt_digest - ), + complete_reentry_result_digest=(result.receipt.complete_reentry_result_digest), + complete_reentry_receipt_digest=(result.receipt.complete_reentry_receipt_digest), closeout_report_digest=result.receipt.closeout_report_digest, - closeout_workflow_operation_digest=( - result.receipt.closeout_workflow_operation_digest - ), + closeout_workflow_operation_digest=(result.receipt.closeout_workflow_operation_digest), before_state_digest=result.receipt.before_state_digest, after_state_digest=result.receipt.after_state_digest, before_control_plane_digest=result.receipt.before_control_plane_digest, @@ -329,8 +320,7 @@ def create( ) if entry.sequence <= previous_sequence: raise FoundationError( - "complete reentry closeout coordination ledger sequences " - "must increase" + "complete reentry closeout coordination ledger sequences must increase" ) seen_sequences.add(entry.sequence) @@ -351,9 +341,7 @@ def append( entry: CompleteHumanReviewReentryCloseoutCoordinationLedgerEntry, ) -> CompleteHumanReviewReentryCloseoutCoordinationLedger: """Return a new ledger with an appended coordination entry.""" - return CompleteHumanReviewReentryCloseoutCoordinationLedger.create( - (*self.entries, entry) - ) + return CompleteHumanReviewReentryCloseoutCoordinationLedger.create((*self.entries, entry)) def append_result( self, @@ -423,9 +411,7 @@ def entries_for_closeout_status( status: CompleteHumanReviewReentryCloseoutStatus, ) -> tuple[CompleteHumanReviewReentryCloseoutCoordinationLedgerEntry, ...]: """Return coordination entries matching the requested closeout status.""" - return tuple( - entry for entry in self.entries if entry.matches_closeout_status(status) - ) + return tuple(entry for entry in self.entries if entry.matches_closeout_status(status)) def to_payload(self) -> JsonObject: """Return a stable JSON-compatible closeout coordination ledger.""" @@ -446,9 +432,7 @@ def to_payload(self) -> JsonObject: "operator_attention_entry_count": len(self.operator_attention_entries()), "changed_state_entry_count": len(self.changed_state_entries()), "recorded_closeout_entry_count": len(self.recorded_closeout_entries()), - "forge_dispatch_entry_count": len( - self.entries_for_stage(RunStage.FORGE_DISPATCH) - ), + "forge_dispatch_entry_count": len(self.entries_for_stage(RunStage.FORGE_DISPATCH)), "forge_result_processing_entry_count": len( self.entries_for_stage(RunStage.FORGE_RESULT_PROCESSING) ), diff --git a/src/ix_sally/human_review_complete_reentry_closeout_export.py b/src/ix_sally/human_review_complete_reentry_closeout_export.py index b04b19f..44ee5d0 100644 --- a/src/ix_sally/human_review_complete_reentry_closeout_export.py +++ b/src/ix_sally/human_review_complete_reentry_closeout_export.py @@ -171,14 +171,12 @@ def from_result( result: CompleteHumanReviewReentryCloseoutCoordinationResult, ) -> CompleteHumanReviewReentryCloseoutExportPacket: """Create a complete reentry closeout export packet from a coordination result.""" - closeout_ledger = CompleteHumanReviewReentryCloseoutLedger.create( - () - ).append_report(result.closeout_report) - coordination_ledger = ( - CompleteHumanReviewReentryCloseoutCoordinationLedger.create( - () - ).append_result(result) + closeout_ledger = CompleteHumanReviewReentryCloseoutLedger.create(()).append_report( + result.closeout_report ) + coordination_ledger = CompleteHumanReviewReentryCloseoutCoordinationLedger.create( + () + ).append_result(result) artifacts = ( CompleteHumanReviewReentryCloseoutExportArtifact.create( @@ -211,9 +209,7 @@ def from_result( coordination_result_digest=result.digest(), coordination_receipt_digest=result.receipt.digest(), closeout_report_digest=result.closeout_report.digest(), - closeout_workflow_operation_digest=( - result.closeout_workflow_operation.digest() - ), + closeout_workflow_operation_digest=(result.closeout_workflow_operation.digest()), final_state_digest=result.state.digest(), final_control_plane_digest=result.control_plane.digest(), closeout_ledger_digest=closeout_ledger.digest(), @@ -234,9 +230,7 @@ def required_artifacts( def missing_required_artifact_labels(self) -> tuple[str, ...]: """Return labels for required artifacts whose digest is empty.""" return tuple( - artifact.label - for artifact in self.required_artifacts() - if not artifact.digest.value + artifact.label for artifact in self.required_artifacts() if not artifact.digest.value ) def complete(self) -> bool: @@ -293,9 +287,7 @@ def to_payload(self) -> JsonObject: "requires_operator_attention": self.requires_operator_attention, "artifact_count": len(self.artifacts), "required_artifact_count": len(self.required_artifacts()), - "missing_required_artifact_labels": list( - self.missing_required_artifact_labels() - ), + "missing_required_artifact_labels": list(self.missing_required_artifact_labels()), "complete": self.complete(), "exportable_without_operator": self.exportable_without_operator(), "artifacts": artifact_payload, diff --git a/src/ix_sally/human_review_complete_reentry_ledger.py b/src/ix_sally/human_review_complete_reentry_ledger.py index 24810e7..99ec400 100644 --- a/src/ix_sally/human_review_complete_reentry_ledger.py +++ b/src/ix_sally/human_review_complete_reentry_ledger.py @@ -2,8 +2,9 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass -from typing import TYPE_CHECKING, Iterable +from typing import TYPE_CHECKING from ix_sally.digest import DigestRecord, JsonArray, JsonObject from ix_sally.foundation import CanonicalKey, FoundationError @@ -11,11 +12,11 @@ if TYPE_CHECKING: from ix_sally.human_review_complete_reentry import CompleteHumanReviewReentryResult - from ix_sally.human_review_control_plane_report import ( + from ix_sally.human_review_control_plane_report_status import ( HumanReviewControlPlaneReportStatus, ) - from ix_sally.human_review_reentry import HumanReviewReentryStatus - from ix_sally.human_review_reentry_audit import HumanReviewReentryAuditStatus + from ix_sally.human_review_reentry_audit_status import HumanReviewReentryAuditStatus + from ix_sally.human_review_reentry_status import HumanReviewReentryStatus @dataclass(frozen=True, slots=True) @@ -68,13 +69,9 @@ def create( ) -> CompleteHumanReviewReentryLedgerEntry: """Create a normalized complete human-review reentry ledger entry.""" if sequence <= 0: - raise FoundationError( - "complete human-review reentry ledger sequence must be positive" - ) + raise FoundationError("complete human-review reentry ledger sequence must be positive") if max_steps <= 0: - raise FoundationError( - "complete human-review reentry ledger max_steps must be positive" - ) + raise FoundationError("complete human-review reentry ledger max_steps must be positive") if executed_steps < 0: raise FoundationError( "complete human-review reentry ledger executed_steps must not be negative" @@ -138,9 +135,7 @@ def from_result( resume_operation_digest=result.receipt.resume_operation_digest, audited_reentry_result_digest=result.receipt.audited_reentry_result_digest, audited_reentry_receipt_digest=result.receipt.audited_reentry_receipt_digest, - final_workflow_operation_digest=( - result.receipt.final_workflow_operation_digest - ), + final_workflow_operation_digest=(result.receipt.final_workflow_operation_digest), before_state_digest=result.receipt.before_state_digest, after_state_digest=result.receipt.after_state_digest, before_control_plane_digest=result.receipt.before_control_plane_digest, @@ -273,9 +268,7 @@ def to_payload(self) -> JsonObject: "executed_steps": self.executed_steps, "changed_state": self.changed_state(), "recorded_reentry_and_audit": self.recorded_reentry_and_audit(), - "recorded_complete_audited_reentry": ( - self.recorded_complete_audited_reentry() - ), + "recorded_complete_audited_reentry": (self.recorded_complete_audited_reentry()), "accepted": self.accepted(), "failed": self.failed(), "waiting_for_external_input": self.waiting_for_external_input(), @@ -308,8 +301,7 @@ def create( for entry in normalized: if entry.sequence in seen_sequences: raise FoundationError( - f"duplicate complete human-review reentry ledger sequence: " - f"{entry.sequence}" + f"duplicate complete human-review reentry ledger sequence: {entry.sequence}" ) if entry.entry_id.value in seen_entry_ids: raise FoundationError( @@ -413,9 +405,7 @@ def to_payload(self) -> JsonObject: "waiting_entry_count": len(self.waiting_entries()), "operator_attention_entry_count": len(self.operator_attention_entries()), "changed_state_entry_count": len(self.changed_state_entries()), - "forge_dispatch_entry_count": len( - self.entries_for_stage(RunStage.FORGE_DISPATCH) - ), + "forge_dispatch_entry_count": len(self.entries_for_stage(RunStage.FORGE_DISPATCH)), "forge_result_processing_entry_count": len( self.entries_for_stage(RunStage.FORGE_RESULT_PROCESSING) ), diff --git a/src/ix_sally/human_review_complete_reentry_report.py b/src/ix_sally/human_review_complete_reentry_report.py index 238157d..cb2d47f 100644 --- a/src/ix_sally/human_review_complete_reentry_report.py +++ b/src/ix_sally/human_review_complete_reentry_report.py @@ -7,12 +7,12 @@ from ix_sally.digest import DigestRecord, JsonObject from ix_sally.foundation import CanonicalKey, FoundationError, require_text -from ix_sally.human_review_complete_reentry_ledger import ( - CompleteHumanReviewReentryLedgerEntry, -) from ix_sally.human_review_complete_reentry_closeout_status import ( CompleteHumanReviewReentryCloseoutStatus, ) +from ix_sally.human_review_complete_reentry_ledger import ( + CompleteHumanReviewReentryLedgerEntry, +) from ix_sally.human_review_control_plane_report_status import ( HumanReviewControlPlaneReportStatus, ) @@ -124,8 +124,7 @@ def create( ) if executed_steps < 0: raise FoundationError( - "complete human-review reentry closeout executed_steps " - "must not be negative" + "complete human-review reentry closeout executed_steps must not be negative" ) if executed_steps > max_steps: raise FoundationError( @@ -140,8 +139,7 @@ def create( }.items(): if value < 0: raise FoundationError( - f"complete human-review reentry closeout {field_name} " - "must not be negative" + f"complete human-review reentry closeout {field_name} must not be negative" ) complete_reentry_result_digest.require_algorithm("sha256") @@ -153,16 +151,11 @@ def create( has_blocking = any(finding.blocking for finding in findings) if has_blocking and closeout_status is not CompleteHumanReviewReentryCloseoutStatus.BLOCKED: raise FoundationError( - "complete human-review reentry closeout with blocking findings " - "must be blocked" + "complete human-review reentry closeout with blocking findings must be blocked" ) - if ( - not has_blocking - and closeout_status is CompleteHumanReviewReentryCloseoutStatus.BLOCKED - ): + if not has_blocking and closeout_status is CompleteHumanReviewReentryCloseoutStatus.BLOCKED: raise FoundationError( - "complete human-review reentry closeout blocked status requires " - "blocking findings" + "complete human-review reentry closeout blocked status requires blocking findings" ) if complete_reentry_ledger_entry is not None: @@ -191,7 +184,7 @@ def create( raise FoundationError( "complete human-review reentry closeout ledger entry state mismatch" ) - return cls( + return cls( report_id=report_id or CanonicalKey.from_text( f"complete-human-review-reentry-closeout-" @@ -225,11 +218,9 @@ def from_result( result: CompleteHumanReviewReentryResult, ) -> CompleteHumanReviewReentryCloseoutReport: """Create a closeout report from a complete human-review reentry result.""" - complete_reentry_ledger_entry = ( - CompleteHumanReviewReentryLedgerEntry.from_result( - sequence=result.control_plane.complete_reentry_ledger.next_sequence(), - result=result, - ) + complete_reentry_ledger_entry = CompleteHumanReviewReentryLedgerEntry.from_result( + sequence=result.control_plane.complete_reentry_ledger.next_sequence(), + result=result, ) recorded_control_plane = _recorded_control_plane_for_result( result, @@ -331,7 +322,8 @@ def to_payload(self) -> JsonObject: def digest(self) -> DigestRecord: """Return a deterministic digest for this complete reentry closeout report.""" return DigestRecord.from_payload(self.to_payload()) - + + def _select_closeout_status( result: CompleteHumanReviewReentryResult, findings: tuple[CompleteHumanReviewReentryCloseoutFinding, ...], @@ -362,13 +354,8 @@ def _complete_reentry_report_status( status = result.report_status() if status is HumanReviewControlPlaneReportStatus.AUDITED_REENTRY_ACCEPTED: return HumanReviewControlPlaneReportStatus.COMPLETE_REENTRY_ACCEPTED - if ( - status - is HumanReviewControlPlaneReportStatus.AUDITED_REENTRY_WAITING_FOR_EXTERNAL_INPUT - ): - return ( - HumanReviewControlPlaneReportStatus.COMPLETE_REENTRY_WAITING_FOR_EXTERNAL_INPUT - ) + if status is HumanReviewControlPlaneReportStatus.AUDITED_REENTRY_WAITING_FOR_EXTERNAL_INPUT: + return HumanReviewControlPlaneReportStatus.COMPLETE_REENTRY_WAITING_FOR_EXTERNAL_INPUT if status is HumanReviewControlPlaneReportStatus.AUDITED_REENTRY_FAILED: return HumanReviewControlPlaneReportStatus.COMPLETE_REENTRY_FAILED return status diff --git a/src/ix_sally/human_review_complete_reentry_report_ledger.py b/src/ix_sally/human_review_complete_reentry_report_ledger.py index a31cb0e..39ad1ef 100644 --- a/src/ix_sally/human_review_complete_reentry_report_ledger.py +++ b/src/ix_sally/human_review_complete_reentry_report_ledger.py @@ -2,12 +2,13 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass -from typing import TYPE_CHECKING, Iterable +from typing import TYPE_CHECKING from ix_sally.digest import DigestRecord, JsonArray, JsonObject from ix_sally.foundation import CanonicalKey, FoundationError -from ix_sally.human_review_complete_reentry_report import ( +from ix_sally.human_review_complete_reentry_closeout_status import ( CompleteHumanReviewReentryCloseoutStatus, ) from ix_sally.stage_readiness import RunStage @@ -16,11 +17,11 @@ from ix_sally.human_review_complete_reentry_report import ( CompleteHumanReviewReentryCloseoutReport, ) - from ix_sally.human_review_control_plane_report import ( + from ix_sally.human_review_control_plane_report_status import ( HumanReviewControlPlaneReportStatus, ) - from ix_sally.human_review_reentry import HumanReviewReentryStatus - from ix_sally.human_review_reentry_audit import HumanReviewReentryAuditStatus + from ix_sally.human_review_reentry_audit_status import HumanReviewReentryAuditStatus + from ix_sally.human_review_reentry_status import HumanReviewReentryStatus @dataclass(frozen=True, slots=True) @@ -78,23 +79,19 @@ def create( """Create a normalized complete reentry closeout ledger entry.""" if sequence <= 0: raise FoundationError( - "complete human-review reentry closeout ledger sequence " - "must be positive" + "complete human-review reentry closeout ledger sequence must be positive" ) if max_steps <= 0: raise FoundationError( - "complete human-review reentry closeout ledger max_steps " - "must be positive" + "complete human-review reentry closeout ledger max_steps must be positive" ) if executed_steps < 0: raise FoundationError( - "complete human-review reentry closeout ledger executed_steps " - "must not be negative" + "complete human-review reentry closeout ledger executed_steps must not be negative" ) if executed_steps > max_steps: raise FoundationError( - "complete human-review reentry closeout ledger executed_steps " - "exceeds max_steps" + "complete human-review reentry closeout ledger executed_steps exceeds max_steps" ) for field_name, value in { @@ -296,13 +293,11 @@ def create( for entry in normalized: if entry.sequence in seen_sequences: raise FoundationError( - f"duplicate complete reentry closeout ledger sequence: " - f"{entry.sequence}" + f"duplicate complete reentry closeout ledger sequence: {entry.sequence}" ) if entry.entry_id.value in seen_entry_ids: raise FoundationError( - f"duplicate complete reentry closeout ledger entry id: " - f"{entry.entry_id.value}" + f"duplicate complete reentry closeout ledger entry id: {entry.entry_id.value}" ) if entry.closeout_report_digest.value in seen_report_digests: raise FoundationError( @@ -310,9 +305,7 @@ def create( f"{entry.closeout_report_digest.value}" ) if entry.sequence <= previous_sequence: - raise FoundationError( - "complete reentry closeout ledger sequences must increase" - ) + raise FoundationError("complete reentry closeout ledger sequences must increase") seen_sequences.add(entry.sequence) seen_entry_ids.add(entry.entry_id.value) @@ -388,9 +381,7 @@ def entries_for_closeout_status( status: CompleteHumanReviewReentryCloseoutStatus, ) -> tuple[CompleteHumanReviewReentryCloseoutLedgerEntry, ...]: """Return closeout entries matching the requested status.""" - return tuple( - entry for entry in self.entries if entry.matches_closeout_status(status) - ) + return tuple(entry for entry in self.entries if entry.matches_closeout_status(status)) def to_payload(self) -> JsonObject: """Return a stable JSON-compatible closeout ledger.""" @@ -409,9 +400,7 @@ def to_payload(self) -> JsonObject: "waiting_entry_count": len(self.waiting_entries()), "blocked_entry_count": len(self.blocked_entries()), "blocking_finding_entry_count": len(self.blocking_finding_entries()), - "forge_dispatch_entry_count": len( - self.entries_for_stage(RunStage.FORGE_DISPATCH) - ), + "forge_dispatch_entry_count": len(self.entries_for_stage(RunStage.FORGE_DISPATCH)), "forge_result_processing_entry_count": len( self.entries_for_stage(RunStage.FORGE_RESULT_PROCESSING) ), diff --git a/src/ix_sally/human_review_control_plane.py b/src/ix_sally/human_review_control_plane.py index d101d62..b6f3bfe 100644 --- a/src/ix_sally/human_review_control_plane.py +++ b/src/ix_sally/human_review_control_plane.py @@ -53,9 +53,7 @@ def create( reentry_audit_ledger: HumanReviewReentryAuditLedger | None = None, audited_reentry_ledger: AuditedHumanReviewReentryLedger | None = None, complete_reentry_ledger: CompleteHumanReviewReentryLedger | None = None, - complete_reentry_closeout_ledger: ( - CompleteHumanReviewReentryCloseoutLedger | None - ) = None, + complete_reentry_closeout_ledger: (CompleteHumanReviewReentryCloseoutLedger | None) = None, ) -> HumanReviewControlPlaneState: """Create a human-review control-plane state with empty ledgers by default.""" return cls( @@ -63,9 +61,7 @@ def create( decision_ledger=decision_ledger or HumanReviewDecisionLedger.create(()), resume_ledger=resume_ledger or HumanReviewResumeLedger.create(()), reentry_ledger=reentry_ledger or HumanReviewReentryLedger.create(()), - reentry_audit_ledger=( - reentry_audit_ledger or HumanReviewReentryAuditLedger.create(()) - ), + reentry_audit_ledger=(reentry_audit_ledger or HumanReviewReentryAuditLedger.create(())), audited_reentry_ledger=( audited_reentry_ledger or AuditedHumanReviewReentryLedger.create(()) ), @@ -330,20 +326,14 @@ def to_payload(self) -> JsonObject: "reentry_audit_count": self.reentry_audit_count(), "audited_reentry_count": self.audited_reentry_count(), "complete_reentry_count": self.complete_reentry_count(), - "complete_reentry_closeout_count": ( - self.complete_reentry_closeout_count() - ), + "complete_reentry_closeout_count": (self.complete_reentry_closeout_count()), "has_active_handoffs": self.has_active_handoffs(), "has_recorded_decisions": self.has_recorded_decisions(), "has_recorded_resumes": self.has_recorded_resumes(), "has_recorded_reentries": self.has_recorded_reentries(), "has_recorded_reentry_audits": self.has_recorded_reentry_audits(), - "has_recorded_audited_reentries": ( - self.has_recorded_audited_reentries() - ), - "has_recorded_complete_reentries": ( - self.has_recorded_complete_reentries() - ), + "has_recorded_audited_reentries": (self.has_recorded_audited_reentries()), + "has_recorded_complete_reentries": (self.has_recorded_complete_reentries()), "has_recorded_complete_reentry_closeouts": ( self.has_recorded_complete_reentry_closeouts() ), @@ -425,32 +415,16 @@ def from_state( ), completed_reentry_count=len(state.reentry_ledger.changed_entries()), waiting_reentry_count=len( - state.reentry_ledger.entries_by_status_value( - "waiting_for_external_input" - ) - ), - passed_reentry_audit_count=len( - state.reentry_audit_ledger.passed_entries() - ), - failed_reentry_audit_count=len( - state.reentry_audit_ledger.failed_entries() - ), - waiting_reentry_audit_count=len( - state.reentry_audit_ledger.waiting_entries() - ), - blocking_reentry_audit_count=len( - state.reentry_audit_ledger.blocking_entries() + state.reentry_ledger.entries_by_status_value("waiting_for_external_input") ), + passed_reentry_audit_count=len(state.reentry_audit_ledger.passed_entries()), + failed_reentry_audit_count=len(state.reentry_audit_ledger.failed_entries()), + waiting_reentry_audit_count=len(state.reentry_audit_ledger.waiting_entries()), + blocking_reentry_audit_count=len(state.reentry_audit_ledger.blocking_entries()), audited_reentry_count=state.audited_reentry_count(), - accepted_audited_reentry_count=len( - state.audited_reentry_ledger.accepted_entries() - ), - failed_audited_reentry_count=len( - state.audited_reentry_ledger.failed_entries() - ), - waiting_audited_reentry_count=len( - state.audited_reentry_ledger.waiting_entries() - ), + accepted_audited_reentry_count=len(state.audited_reentry_ledger.accepted_entries()), + failed_audited_reentry_count=len(state.audited_reentry_ledger.failed_entries()), + waiting_audited_reentry_count=len(state.audited_reentry_ledger.waiting_entries()), operator_attention_audited_reentry_count=len( state.audited_reentry_ledger.operator_attention_entries() ), @@ -458,24 +432,16 @@ def from_state( state.audited_reentry_ledger.changed_state_entries() ), complete_reentry_count=state.complete_reentry_count(), - accepted_complete_reentry_count=len( - state.complete_reentry_ledger.accepted_entries() - ), - failed_complete_reentry_count=len( - state.complete_reentry_ledger.failed_entries() - ), - waiting_complete_reentry_count=len( - state.complete_reentry_ledger.waiting_entries() - ), + accepted_complete_reentry_count=len(state.complete_reentry_ledger.accepted_entries()), + failed_complete_reentry_count=len(state.complete_reentry_ledger.failed_entries()), + waiting_complete_reentry_count=len(state.complete_reentry_ledger.waiting_entries()), operator_attention_complete_reentry_count=len( state.complete_reentry_ledger.operator_attention_entries() ), changed_state_complete_reentry_count=len( state.complete_reentry_ledger.changed_state_entries() ), - complete_reentry_closeout_count=( - state.complete_reentry_closeout_count() - ), + complete_reentry_closeout_count=(state.complete_reentry_closeout_count()), accepted_complete_reentry_closeout_count=len( state.complete_reentry_closeout_ledger.accepted_entries() ), @@ -626,18 +592,14 @@ def to_payload(self) -> JsonObject: "operator_attention_audited_reentry_count": ( self.operator_attention_audited_reentry_count ), - "changed_state_audited_reentry_count": ( - self.changed_state_audited_reentry_count - ), + "changed_state_audited_reentry_count": (self.changed_state_audited_reentry_count), "accepted_complete_reentry_count": self.accepted_complete_reentry_count, "failed_complete_reentry_count": self.failed_complete_reentry_count, "waiting_complete_reentry_count": self.waiting_complete_reentry_count, "operator_attention_complete_reentry_count": ( self.operator_attention_complete_reentry_count ), - "changed_state_complete_reentry_count": ( - self.changed_state_complete_reentry_count - ), + "changed_state_complete_reentry_count": (self.changed_state_complete_reentry_count), "accepted_complete_reentry_closeout_count": ( self.accepted_complete_reentry_closeout_count ), @@ -660,24 +622,18 @@ def to_payload(self) -> JsonObject: "has_passed_reentry_audit": self.has_passed_reentry_audit(), "has_failed_reentry_audit": self.has_failed_reentry_audit(), "has_blocking_reentry_audit": self.has_blocking_reentry_audit(), - "is_waiting_after_reentry_audit": ( - self.is_waiting_after_reentry_audit() - ), + "is_waiting_after_reentry_audit": (self.is_waiting_after_reentry_audit()), "has_audited_reentry": self.has_audited_reentry(), "has_accepted_audited_reentry": self.has_accepted_audited_reentry(), "has_failed_audited_reentry": self.has_failed_audited_reentry(), - "is_waiting_after_audited_reentry": ( - self.is_waiting_after_audited_reentry() - ), + "is_waiting_after_audited_reentry": (self.is_waiting_after_audited_reentry()), "audited_reentry_requires_operator_attention": ( self.audited_reentry_requires_operator_attention() ), "has_complete_reentry": self.has_complete_reentry(), "has_accepted_complete_reentry": self.has_accepted_complete_reentry(), "has_failed_complete_reentry": self.has_failed_complete_reentry(), - "is_waiting_after_complete_reentry": ( - self.is_waiting_after_complete_reentry() - ), + "is_waiting_after_complete_reentry": (self.is_waiting_after_complete_reentry()), "complete_reentry_requires_operator_attention": ( self.complete_reentry_requires_operator_attention() ), @@ -688,9 +644,7 @@ def to_payload(self) -> JsonObject: "is_waiting_after_complete_reentry_closeout": ( self.is_waiting_after_complete_reentry_closeout() ), - "has_blocked_complete_reentry_closeout": ( - self.has_blocked_complete_reentry_closeout() - ), + "has_blocked_complete_reentry_closeout": (self.has_blocked_complete_reentry_closeout()), "complete_reentry_closeout_has_blocking_findings": ( self.complete_reentry_closeout_has_blocking_findings() ), @@ -732,17 +686,11 @@ def require_consistent(self) -> None: if self.status.reentry_count != self.state.reentry_count(): raise FoundationError("human-review control-plane reentry count mismatch") if self.status.reentry_audit_count != self.state.reentry_audit_count(): - raise FoundationError( - "human-review control-plane reentry audit count mismatch" - ) + raise FoundationError("human-review control-plane reentry audit count mismatch") if self.status.audited_reentry_count != self.state.audited_reentry_count(): - raise FoundationError( - "human-review control-plane audited reentry count mismatch" - ) + raise FoundationError("human-review control-plane audited reentry count mismatch") if self.status.complete_reentry_count != self.state.complete_reentry_count(): - raise FoundationError( - "human-review control-plane complete reentry count mismatch" - ) + raise FoundationError("human-review control-plane complete reentry count mismatch") if ( self.status.complete_reentry_closeout_count != self.state.complete_reentry_closeout_count() @@ -761,15 +709,9 @@ def to_payload(self) -> JsonObject: "decision_ledger_digest": self.state.decision_ledger.digest().value, "resume_ledger_digest": self.state.resume_ledger.digest().value, "reentry_ledger_digest": self.state.reentry_ledger.digest().value, - "reentry_audit_ledger_digest": ( - self.state.reentry_audit_ledger.digest().value - ), - "audited_reentry_ledger_digest": ( - self.state.audited_reentry_ledger.digest().value - ), - "complete_reentry_ledger_digest": ( - self.state.complete_reentry_ledger.digest().value - ), + "reentry_audit_ledger_digest": (self.state.reentry_audit_ledger.digest().value), + "audited_reentry_ledger_digest": (self.state.audited_reentry_ledger.digest().value), + "complete_reentry_ledger_digest": (self.state.complete_reentry_ledger.digest().value), "complete_reentry_closeout_ledger_digest": ( self.state.complete_reentry_closeout_ledger.digest().value ), diff --git a/src/ix_sally/human_review_control_plane_coordinator.py b/src/ix_sally/human_review_control_plane_coordinator.py index 8782064..ea32d99 100644 --- a/src/ix_sally/human_review_control_plane_coordinator.py +++ b/src/ix_sally/human_review_control_plane_coordinator.py @@ -86,21 +86,13 @@ def create( ) -> HumanReviewControlPlaneOperationReceipt: """Create a normalized human-review control-plane operation receipt.""" if handoff_count < 0: - raise FoundationError( - "human-review control-plane handoff_count must not be negative" - ) + raise FoundationError("human-review control-plane handoff_count must not be negative") if decision_count < 0: - raise FoundationError( - "human-review control-plane decision_count must not be negative" - ) + raise FoundationError("human-review control-plane decision_count must not be negative") if resume_count < 0: - raise FoundationError( - "human-review control-plane resume_count must not be negative" - ) + raise FoundationError("human-review control-plane resume_count must not be negative") if reentry_count < 0: - raise FoundationError( - "human-review control-plane reentry_count must not be negative" - ) + raise FoundationError("human-review control-plane reentry_count must not be negative") if reentry_audit_count < 0: raise FoundationError( "human-review control-plane reentry_audit_count must not be negative" @@ -115,8 +107,7 @@ def create( ) if complete_reentry_closeout_count < 0: raise FoundationError( - "human-review control-plane complete_reentry_closeout_count " - "must not be negative" + "human-review control-plane complete_reentry_closeout_count must not be negative" ) before_control_plane_digest.require_algorithm("sha256") @@ -196,9 +187,7 @@ class HumanReviewControlPlaneOperationResult: reentry_audit_report: HumanReviewReentryAuditReport | None = None audited_reentry_result: AuditedHumanReviewReentryResult | None = None complete_reentry_result: CompleteHumanReviewReentryResult | None = None - complete_reentry_closeout_report: ( - CompleteHumanReviewReentryCloseoutReport | None - ) = None + complete_reentry_closeout_report: CompleteHumanReviewReentryCloseoutReport | None = None def changed_control_plane(self) -> bool: """Return whether this operation changed control-plane state.""" @@ -235,17 +224,13 @@ def require_reentry_result(self) -> HumanReviewReentryResult: def require_reentry_audit_report(self) -> HumanReviewReentryAuditReport: """Return the reentry audit report or raise if this was another operation kind.""" if self.reentry_audit_report is None: - raise FoundationError( - "human-review control-plane result has no reentry audit report" - ) + raise FoundationError("human-review control-plane result has no reentry audit report") return self.reentry_audit_report def require_audited_reentry_result(self) -> AuditedHumanReviewReentryResult: """Return the audited reentry result or raise if this was another operation kind.""" if self.audited_reentry_result is None: - raise FoundationError( - "human-review control-plane result has no audited reentry result" - ) + raise FoundationError("human-review control-plane result has no audited reentry result") return self.audited_reentry_result def require_complete_reentry_result(self) -> CompleteHumanReviewReentryResult: @@ -262,8 +247,7 @@ def require_complete_reentry_closeout_report( """Return the complete reentry closeout report or raise.""" if self.complete_reentry_closeout_report is None: raise FoundationError( - "human-review control-plane result has no complete reentry " - "closeout report" + "human-review control-plane result has no complete reentry closeout report" ) return self.complete_reentry_closeout_report @@ -286,24 +270,16 @@ def to_payload(self) -> JsonObject: ), "changed_control_plane": self.changed_control_plane(), "handoff_result_digest": ( - self.handoff_result.digest().value - if self.handoff_result is not None - else None + self.handoff_result.digest().value if self.handoff_result is not None else None ), "decision_result_digest": ( - self.decision_result.digest().value - if self.decision_result is not None - else None + self.decision_result.digest().value if self.decision_result is not None else None ), "resume_result_digest": ( - self.resume_result.digest().value - if self.resume_result is not None - else None + self.resume_result.digest().value if self.resume_result is not None else None ), "reentry_result_digest": ( - self.reentry_result.digest().value - if self.reentry_result is not None - else None + self.reentry_result.digest().value if self.reentry_result is not None else None ), "reentry_audit_report_digest": ( self.reentry_audit_report.digest().value @@ -355,8 +331,7 @@ def record_handoff( run_state: NinefoldRunState, control_plane: HumanReviewControlPlaneState, authority_note: str = ( - "Human authority is required before IX-Sally may treat these " - "targets as resolved." + "Human authority is required before IX-Sally may treat these targets as resolved." ), ) -> HumanReviewControlPlaneOperationResult: """Assemble a human-review handoff and update the control-plane handoff ledger.""" @@ -465,8 +440,7 @@ def record_reentry( """Record a certified human-review reentry result in the reentry ledger.""" if reentry_result.control_plane.digest() != control_plane.digest(): raise FoundationError( - "human-review control-plane reentry result must match current " - "control-plane state" + "human-review control-plane reentry result must match current control-plane state" ) before_digest = control_plane.digest() @@ -495,8 +469,7 @@ def record_reentry_audit( """Record a human-review reentry audit report in the audit ledger.""" if audit_report.control_plane_digest != control_plane.digest(): raise FoundationError( - "human-review control-plane reentry audit must match current " - "control-plane state" + "human-review control-plane reentry audit must match current control-plane state" ) before_digest = control_plane.digest() @@ -525,14 +498,11 @@ def record_audited_reentry( """Record a fully audited human-review reentry result in the control plane.""" if audited_reentry_result.control_plane.digest() != control_plane.digest(): raise FoundationError( - "human-review control-plane audited reentry must match current " - "control-plane state" + "human-review control-plane audited reentry must match current control-plane state" ) before_digest = control_plane.digest() - updated_ledger = control_plane.audited_reentry_ledger.append_result( - audited_reentry_result - ) + updated_ledger = control_plane.audited_reentry_ledger.append_result(audited_reentry_result) updated_control_plane = control_plane.with_audited_reentry_ledger(updated_ledger) receipt = self._receipt( operation_kind=HumanReviewControlPlaneOperationKind.AUDITED_REENTRY_RECORDED, @@ -557,17 +527,14 @@ def record_complete_reentry( """Record a complete human-review reentry result in the control plane.""" if complete_reentry_result.control_plane.digest() != control_plane.digest(): raise FoundationError( - "human-review control-plane complete reentry must match current " - "control-plane state" + "human-review control-plane complete reentry must match current control-plane state" ) before_digest = control_plane.digest() updated_ledger = control_plane.complete_reentry_ledger.append_result( complete_reentry_result ) - updated_control_plane = control_plane.with_complete_reentry_ledger( - updated_ledger - ) + updated_control_plane = control_plane.with_complete_reentry_ledger(updated_ledger) receipt = self._receipt( operation_kind=HumanReviewControlPlaneOperationKind.COMPLETE_REENTRY_RECORDED, before_digest=before_digest, @@ -599,22 +566,18 @@ def record_complete_reentry_closeout( "current control-plane state" ) - expected_sequence = ( - closeout_control_plane.complete_reentry_ledger.next_sequence() - ) + expected_sequence = closeout_control_plane.complete_reentry_ledger.next_sequence() if ledger_entry.sequence != expected_sequence: raise FoundationError( "human-review control-plane complete reentry closeout must match " "current control-plane state" ) - complete_reentry_ledger = ( - closeout_control_plane.complete_reentry_ledger.append(ledger_entry) + complete_reentry_ledger = closeout_control_plane.complete_reentry_ledger.append( + ledger_entry ) - closeout_control_plane = ( - closeout_control_plane.with_complete_reentry_ledger( - complete_reentry_ledger - ) + closeout_control_plane = closeout_control_plane.with_complete_reentry_ledger( + complete_reentry_ledger ) if closeout_report.control_plane_digest != closeout_control_plane.digest(): @@ -624,13 +587,11 @@ def record_complete_reentry_closeout( ) before_digest = closeout_control_plane.digest() - updated_ledger = ( - closeout_control_plane.complete_reentry_closeout_ledger.append_report( - closeout_report - ) + updated_ledger = closeout_control_plane.complete_reentry_closeout_ledger.append_report( + closeout_report ) - updated_control_plane = ( - closeout_control_plane.with_complete_reentry_closeout_ledger(updated_ledger) + updated_control_plane = closeout_control_plane.with_complete_reentry_closeout_ledger( + updated_ledger ) receipt = self._receipt( operation_kind=( @@ -669,7 +630,5 @@ def _receipt( reentry_audit_count=after_control_plane.reentry_audit_count(), audited_reentry_count=after_control_plane.audited_reentry_count(), complete_reentry_count=after_control_plane.complete_reentry_count(), - complete_reentry_closeout_count=( - after_control_plane.complete_reentry_closeout_count() - ), + complete_reentry_closeout_count=(after_control_plane.complete_reentry_closeout_count()), ) diff --git a/src/ix_sally/human_review_control_plane_report.py b/src/ix_sally/human_review_control_plane_report.py index 7cd6b88..66062a4 100644 --- a/src/ix_sally/human_review_control_plane_report.py +++ b/src/ix_sally/human_review_control_plane_report.py @@ -149,12 +149,8 @@ def create( "accepted_audited_reentry_count": accepted_audited_reentry_count, "failed_audited_reentry_count": failed_audited_reentry_count, "waiting_audited_reentry_count": waiting_audited_reentry_count, - "operator_attention_audited_reentry_count": ( - operator_attention_audited_reentry_count - ), - "changed_state_audited_reentry_count": ( - changed_state_audited_reentry_count - ), + "operator_attention_audited_reentry_count": (operator_attention_audited_reentry_count), + "changed_state_audited_reentry_count": (changed_state_audited_reentry_count), "complete_reentry_count": complete_reentry_count, "accepted_complete_reentry_count": accepted_complete_reentry_count, "failed_complete_reentry_count": failed_complete_reentry_count, @@ -162,27 +158,18 @@ def create( "operator_attention_complete_reentry_count": ( operator_attention_complete_reentry_count ), - "changed_state_complete_reentry_count": ( - changed_state_complete_reentry_count - ), + "changed_state_complete_reentry_count": (changed_state_complete_reentry_count), "complete_reentry_closeout_count": complete_reentry_closeout_count, - "accepted_complete_reentry_closeout_count": ( - accepted_complete_reentry_closeout_count - ), - "waiting_complete_reentry_closeout_count": ( - waiting_complete_reentry_closeout_count - ), - "blocked_complete_reentry_closeout_count": ( - blocked_complete_reentry_closeout_count - ), + "accepted_complete_reentry_closeout_count": (accepted_complete_reentry_closeout_count), + "waiting_complete_reentry_closeout_count": (waiting_complete_reentry_closeout_count), + "blocked_complete_reentry_closeout_count": (blocked_complete_reentry_closeout_count), "blocking_finding_complete_reentry_closeout_count": ( blocking_finding_complete_reentry_closeout_count ), }.items(): if value < 0: raise FoundationError( - f"human-review control-plane report {field_name} " - "must not be negative" + f"human-review control-plane report {field_name} must not be negative" ) if ( @@ -190,44 +177,34 @@ def create( > decision_count ): raise FoundationError( - "human-review control-plane report decision subtotals exceed " - "decision_count" + "human-review control-plane report decision subtotals exceed decision_count" ) if cleared_resume_count > resume_count: raise FoundationError( - "human-review control-plane report cleared_resume_count exceeds " - "resume_count" + "human-review control-plane report cleared_resume_count exceeds resume_count" ) if completed_reentry_count > reentry_count: raise FoundationError( - "human-review control-plane report completed_reentry_count exceeds " - "reentry_count" + "human-review control-plane report completed_reentry_count exceeds reentry_count" ) if waiting_reentry_count > reentry_count: raise FoundationError( - "human-review control-plane report waiting_reentry_count exceeds " - "reentry_count" + "human-review control-plane report waiting_reentry_count exceeds reentry_count" ) if ( - passed_reentry_audit_count - + failed_reentry_audit_count - + waiting_reentry_audit_count + passed_reentry_audit_count + failed_reentry_audit_count + waiting_reentry_audit_count > reentry_audit_count ): raise FoundationError( "human-review control-plane report reentry audit subtotals exceed " "reentry_audit_count" ) - if blocking_reentry_audit_count > reentry_audit_count: + if blocking_reentry_audit_count > reentry_audit_count: raise FoundationError( "human-review control-plane report blocking_reentry_audit_count " "exceeds reentry_audit_count" ) - if ( - accepted_audited_reentry_count - + failed_audited_reentry_count - > audited_reentry_count - ): + if accepted_audited_reentry_count + failed_audited_reentry_count > audited_reentry_count: raise FoundationError( "human-review control-plane report audited reentry subtotals exceed " "audited_reentry_count" @@ -247,10 +224,7 @@ def create( "human-review control-plane report " "changed_state_audited_reentry_count exceeds audited_reentry_count" ) - if ( - accepted_complete_reentry_count + failed_complete_reentry_count - > complete_reentry_count - ): + if accepted_complete_reentry_count + failed_complete_reentry_count > complete_reentry_count: raise FoundationError( "human-review control-plane report complete reentry subtotals " "exceed complete_reentry_count" @@ -281,10 +255,7 @@ def create( "human-review control-plane report complete closeout subtotals " "exceed complete_reentry_closeout_count" ) - if ( - blocking_finding_complete_reentry_closeout_count - > complete_reentry_closeout_count - ): + if blocking_finding_complete_reentry_closeout_count > complete_reentry_closeout_count: raise FoundationError( "human-review control-plane report complete closeout blocking " "finding count exceeds complete_reentry_closeout_count" @@ -334,38 +305,24 @@ def create( accepted_audited_reentry_count=accepted_audited_reentry_count, failed_audited_reentry_count=failed_audited_reentry_count, waiting_audited_reentry_count=waiting_audited_reentry_count, - operator_attention_audited_reentry_count=( - operator_attention_audited_reentry_count - ), - changed_state_audited_reentry_count=( - changed_state_audited_reentry_count - ), + operator_attention_audited_reentry_count=(operator_attention_audited_reentry_count), + changed_state_audited_reentry_count=(changed_state_audited_reentry_count), latest_audited_reentry_digest=latest_audited_reentry_digest, complete_reentry_count=complete_reentry_count, accepted_complete_reentry_count=accepted_complete_reentry_count, failed_complete_reentry_count=failed_complete_reentry_count, waiting_complete_reentry_count=waiting_complete_reentry_count, - operator_attention_complete_reentry_count=( - operator_attention_complete_reentry_count - ), + operator_attention_complete_reentry_count=(operator_attention_complete_reentry_count), changed_state_complete_reentry_count=changed_state_complete_reentry_count, latest_complete_reentry_digest=latest_complete_reentry_digest, complete_reentry_closeout_count=complete_reentry_closeout_count, - accepted_complete_reentry_closeout_count=( - accepted_complete_reentry_closeout_count - ), - waiting_complete_reentry_closeout_count=( - waiting_complete_reentry_closeout_count - ), - blocked_complete_reentry_closeout_count=( - blocked_complete_reentry_closeout_count - ), + accepted_complete_reentry_closeout_count=(accepted_complete_reentry_closeout_count), + waiting_complete_reentry_closeout_count=(waiting_complete_reentry_closeout_count), + blocked_complete_reentry_closeout_count=(blocked_complete_reentry_closeout_count), blocking_finding_complete_reentry_closeout_count=( blocking_finding_complete_reentry_closeout_count ), - latest_complete_reentry_closeout_digest=( - latest_complete_reentry_closeout_digest - ), + latest_complete_reentry_closeout_digest=(latest_complete_reentry_closeout_digest), ) @classmethod @@ -402,19 +359,11 @@ def from_snapshots( passed_reentry_audit_count=control_status.passed_reentry_audit_count, failed_reentry_audit_count=control_status.failed_reentry_audit_count, waiting_reentry_audit_count=control_status.waiting_reentry_audit_count, - blocking_reentry_audit_count=( - control_status.blocking_reentry_audit_count - ), + blocking_reentry_audit_count=(control_status.blocking_reentry_audit_count), audited_reentry_count=control_status.audited_reentry_count, - accepted_audited_reentry_count=( - control_status.accepted_audited_reentry_count - ), - failed_audited_reentry_count=( - control_status.failed_audited_reentry_count - ), - waiting_audited_reentry_count=( - control_status.waiting_audited_reentry_count - ), + accepted_audited_reentry_count=(control_status.accepted_audited_reentry_count), + failed_audited_reentry_count=(control_status.failed_audited_reentry_count), + waiting_audited_reentry_count=(control_status.waiting_audited_reentry_count), operator_attention_audited_reentry_count=( control_status.operator_attention_audited_reentry_count ), @@ -422,23 +371,17 @@ def from_snapshots( control_status.changed_state_audited_reentry_count ), complete_reentry_count=control_status.complete_reentry_count, - accepted_complete_reentry_count=( - control_status.accepted_complete_reentry_count - ), + accepted_complete_reentry_count=(control_status.accepted_complete_reentry_count), failed_complete_reentry_count=control_status.failed_complete_reentry_count, - waiting_complete_reentry_count=( - control_status.waiting_complete_reentry_count - ), + waiting_complete_reentry_count=(control_status.waiting_complete_reentry_count), operator_attention_complete_reentry_count=( control_status.operator_attention_complete_reentry_count ), changed_state_complete_reentry_count=( control_status.changed_state_complete_reentry_count ), - complete_reentry_closeout_count=( - control_status.complete_reentry_closeout_count - ), - accepted_complete_reentry_closeout_count=( + complete_reentry_closeout_count=(control_status.complete_reentry_closeout_count), + accepted_complete_reentry_closeout_count=( control_status.accepted_complete_reentry_closeout_count ), waiting_complete_reentry_closeout_count=( @@ -658,18 +601,14 @@ def to_payload(self) -> JsonObject: "operator_attention_audited_reentry_count": ( self.operator_attention_audited_reentry_count ), - "changed_state_audited_reentry_count": ( - self.changed_state_audited_reentry_count - ), + "changed_state_audited_reentry_count": (self.changed_state_audited_reentry_count), "accepted_complete_reentry_count": self.accepted_complete_reentry_count, "failed_complete_reentry_count": self.failed_complete_reentry_count, "waiting_complete_reentry_count": self.waiting_complete_reentry_count, "operator_attention_complete_reentry_count": ( self.operator_attention_complete_reentry_count ), - "changed_state_complete_reentry_count": ( - self.changed_state_complete_reentry_count - ), + "changed_state_complete_reentry_count": (self.changed_state_complete_reentry_count), "accepted_complete_reentry_closeout_count": ( self.accepted_complete_reentry_closeout_count ), @@ -705,15 +644,9 @@ def to_payload(self) -> JsonObject: "complete_reentry_recorded": self.complete_reentry_recorded(), "complete_reentry_accepted": self.complete_reentry_accepted(), "complete_reentry_failed": self.complete_reentry_failed(), - "complete_reentry_closeout_recorded": ( - self.complete_reentry_closeout_recorded() - ), - "complete_reentry_closeout_accepted": ( - self.complete_reentry_closeout_accepted() - ), - "complete_reentry_closeout_blocked": ( - self.complete_reentry_closeout_blocked() - ), + "complete_reentry_closeout_recorded": (self.complete_reentry_closeout_recorded()), + "complete_reentry_closeout_accepted": (self.complete_reentry_closeout_accepted()), + "complete_reentry_closeout_blocked": (self.complete_reentry_closeout_blocked()), "has_handoff": self.has_handoff(), "has_decision": self.has_decision(), "has_resume": self.has_resume(), @@ -738,9 +671,7 @@ def report( """Return a receipt-grade report for the current run and control-plane state.""" return HumanReviewControlPlaneReport.from_snapshots( run_snapshot=RunStageSnapshot.from_state(run_state), - control_plane_snapshot=HumanReviewControlPlaneSnapshot.from_state( - control_plane - ), + control_plane_snapshot=HumanReviewControlPlaneSnapshot.from_state(control_plane), ) @@ -802,10 +733,7 @@ def _select_report_status( "Complete human-review reentry was accepted and recorded.", ) - if ( - status.audited_reentry_requires_operator_attention() - or status.has_failed_audited_reentry() - ): + if status.audited_reentry_requires_operator_attention() or status.has_failed_audited_reentry(): return ( HumanReviewControlPlaneReportStatus.AUDITED_REENTRY_FAILED, "A fully audited human-review reentry requires operator attention.", diff --git a/src/ix_sally/human_review_control_plane_report_status.py b/src/ix_sally/human_review_control_plane_report_status.py index 63b8167..0ab11e2 100644 --- a/src/ix_sally/human_review_control_plane_report_status.py +++ b/src/ix_sally/human_review_control_plane_report_status.py @@ -13,19 +13,13 @@ class HumanReviewControlPlaneReportStatus(StrEnum): REENTRY_RECORDED = "reentry_recorded" REENTRY_WAITING_FOR_EXTERNAL_INPUT = "reentry_waiting_for_external_input" REENTRY_AUDIT_PASSED = "reentry_audit_passed" - REENTRY_AUDIT_WAITING_FOR_EXTERNAL_INPUT = ( - "reentry_audit_waiting_for_external_input" - ) + REENTRY_AUDIT_WAITING_FOR_EXTERNAL_INPUT = "reentry_audit_waiting_for_external_input" REENTRY_AUDIT_FAILED = "reentry_audit_failed" AUDITED_REENTRY_ACCEPTED = "audited_reentry_accepted" - AUDITED_REENTRY_WAITING_FOR_EXTERNAL_INPUT = ( - "audited_reentry_waiting_for_external_input" - ) + AUDITED_REENTRY_WAITING_FOR_EXTERNAL_INPUT = "audited_reentry_waiting_for_external_input" AUDITED_REENTRY_FAILED = "audited_reentry_failed" COMPLETE_REENTRY_ACCEPTED = "complete_reentry_accepted" - COMPLETE_REENTRY_WAITING_FOR_EXTERNAL_INPUT = ( - "complete_reentry_waiting_for_external_input" - ) + COMPLETE_REENTRY_WAITING_FOR_EXTERNAL_INPUT = "complete_reentry_waiting_for_external_input" COMPLETE_REENTRY_FAILED = "complete_reentry_failed" COMPLETE_REENTRY_CLOSEOUT_ACCEPTED = "complete_reentry_closeout_accepted" COMPLETE_REENTRY_CLOSEOUT_WAITING_FOR_EXTERNAL_INPUT = ( diff --git a/src/ix_sally/human_review_decision_ledger.py b/src/ix_sally/human_review_decision_ledger.py index 6fc0308..b6888d1 100644 --- a/src/ix_sally/human_review_decision_ledger.py +++ b/src/ix_sally/human_review_decision_ledger.py @@ -2,8 +2,8 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass -from typing import Iterable from ix_sally.digest import DigestRecord, JsonArray, JsonObject from ix_sally.foundation import CanonicalKey, FoundationError @@ -205,23 +205,18 @@ def create( for entry in normalized: if entry.sequence in seen_sequences: raise FoundationError( - f"duplicate human-review decision ledger sequence: " - f"{entry.sequence}" + f"duplicate human-review decision ledger sequence: {entry.sequence}" ) if entry.entry_id.value in seen_entry_ids: raise FoundationError( - f"duplicate human-review decision ledger entry id: " - f"{entry.entry_id.value}" + f"duplicate human-review decision ledger entry id: {entry.entry_id.value}" ) if entry.decision_digest.value in seen_decision_digests: raise FoundationError( - f"duplicate human-review decision digest: " - f"{entry.decision_digest.value}" + f"duplicate human-review decision digest: {entry.decision_digest.value}" ) if entry.sequence <= previous_sequence: - raise FoundationError( - "human-review decision ledger sequences must increase" - ) + raise FoundationError("human-review decision ledger sequences must increase") seen_sequences.add(entry.sequence) seen_entry_ids.add(entry.entry_id.value) diff --git a/src/ix_sally/human_review_docket.py b/src/ix_sally/human_review_docket.py index 18143c9..541c8fe 100644 --- a/src/ix_sally/human_review_docket.py +++ b/src/ix_sally/human_review_docket.py @@ -2,9 +2,9 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass from enum import StrEnum -from typing import Iterable from ix_sally.actions import ActionStatus, BoundedActionRecord from ix_sally.cycles import NinefoldCyclePacket @@ -12,7 +12,7 @@ from ix_sally.evidence_support import EvidenceSupportFinding from ix_sally.forge_results import ForgeResultRecord, ForgeResultStatus from ix_sally.foundation import CanonicalKey, FoundationError, require_text -from ix_sally.stage_gate import RunStageGate, StageGateDecision +from ix_sally.stage_gate import RunStageGate from ix_sally.stage_readiness import RunStage, RunStageSnapshot from ix_sally.state import NinefoldRunState @@ -132,9 +132,7 @@ def from_forge_result(cls, result: ForgeResultRecord) -> HumanReviewDocketTarget else HumanReviewDocketSeverity.REVIEW_REQUIRED ) rationale = ( - result.boundary_note - or result.failure_reason - or "Forge result requires human review." + result.boundary_note or result.failure_reason or "Forge result requires human review." ) return cls.create( @@ -295,9 +293,7 @@ def to_payload(self) -> JsonObject: self.targets_by_type(HumanReviewDocketTargetType.BOUNDED_ACTION) ), "evidence_support_finding_count": len( - self.targets_by_type( - HumanReviewDocketTargetType.EVIDENCE_SUPPORT_FINDING - ) + self.targets_by_type(HumanReviewDocketTargetType.EVIDENCE_SUPPORT_FINDING) ), "forge_result_count": len( self.targets_by_type(HumanReviewDocketTargetType.FORGE_RESULT) diff --git a/src/ix_sally/human_review_gateway.py b/src/ix_sally/human_review_gateway.py index 64882d4..641beba 100644 --- a/src/ix_sally/human_review_gateway.py +++ b/src/ix_sally/human_review_gateway.py @@ -289,11 +289,7 @@ def decide_action( decision=decision, ) updated_state = self._record_decision_event( - state=( - state - if after_action == before_action - else state.replace_action(after_action) - ), + state=(state if after_action == before_action else state.replace_action(after_action)), before_action=before_action, after_action=after_action, decision=decision, @@ -327,9 +323,7 @@ def _apply_decision_to_action( ) -> BoundedActionRecord: """Return the bounded action after applying the human decision.""" if action.authority_decision_digest is None: - raise FoundationError( - "human-review actions require an authority decision digest" - ) + raise FoundationError("human-review actions require an authority decision digest") if decision.approves_target(): return BoundedActionRecord.create( diff --git a/src/ix_sally/human_review_handoff.py b/src/ix_sally/human_review_handoff.py index c7a607f..28bd8ac 100644 --- a/src/ix_sally/human_review_handoff.py +++ b/src/ix_sally/human_review_handoff.py @@ -67,14 +67,10 @@ def create( ) surfaced_count = ( - gateway_resolvable_count - + manual_investigation_count - + blocker_acknowledgment_count + gateway_resolvable_count + manual_investigation_count + blocker_acknowledgment_count ) if surfaced_count != target_count: - raise FoundationError( - "human-review handoff surfaced counts must equal target_count" - ) + raise FoundationError("human-review handoff surfaced counts must equal target_count") before_ledger_digest.require_algorithm("sha256") after_ledger_digest.require_algorithm("sha256") @@ -243,8 +239,7 @@ def handoff( state: NinefoldRunState, ledger: HumanReviewBundleLedger, authority_note: str = ( - "Human authority is required before IX-Sally may treat these " - "targets as resolved." + "Human authority is required before IX-Sally may treat these targets as resolved." ), ) -> HumanReviewHandoffResult: """Assemble and append one human-review handoff to the provided ledger.""" diff --git a/src/ix_sally/human_review_packets.py b/src/ix_sally/human_review_packets.py index 382e1b7..ce566bf 100644 --- a/src/ix_sally/human_review_packets.py +++ b/src/ix_sally/human_review_packets.py @@ -2,9 +2,9 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass from enum import StrEnum -from typing import Iterable from ix_sally.digest import DigestRecord, JsonArray, JsonObject from ix_sally.foundation import CanonicalKey, FoundationError, require_text @@ -65,8 +65,7 @@ def create( target_digest.require_algorithm("sha256") normalized_options = tuple( - require_text(option, field_name="decision_option") - for option in decision_options + require_text(option, field_name="decision_option") for option in decision_options ) if len(set(normalized_options)) != len(normalized_options): raise FoundationError("human-review packet card decision options must be unique") @@ -195,9 +194,7 @@ def create( seen_cards: set[str] = set() for card in normalized_cards: if card.card_id.value in seen_cards: - raise FoundationError( - f"duplicate human-review packet card: {card.card_id.value}" - ) + raise FoundationError(f"duplicate human-review packet card: {card.card_id.value}") seen_cards.add(card.card_id.value) normalized_note = require_text(authority_note, field_name="authority_note") @@ -222,8 +219,7 @@ def from_docket( docket: HumanReviewDocket, *, authority_note: str = ( - "Human authority is required before IX-Sally may treat these " - "targets as resolved." + "Human authority is required before IX-Sally may treat these targets as resolved." ), ) -> HumanReviewPacket: """Create an operator packet from a human-review docket.""" diff --git a/src/ix_sally/human_review_reentry.py b/src/ix_sally/human_review_reentry.py index 02794f0..919aa83 100644 --- a/src/ix_sally/human_review_reentry.py +++ b/src/ix_sally/human_review_reentry.py @@ -222,27 +222,16 @@ def _require_resume_operation( resume_operation: HumanReviewWorkflowOperation, ) -> None: """Raise unless the workflow operation is a cleared resume operation.""" - if ( - resume_operation.receipt.workflow_stage - is not HumanReviewWorkflowStage.RESUME_RECORDED - ): - raise FoundationError( - "human-review reentry requires a resume-recorded operation" - ) + if resume_operation.receipt.workflow_stage is not HumanReviewWorkflowStage.RESUME_RECORDED: + raise FoundationError("human-review reentry requires a resume-recorded operation") resume_result = resume_operation.require_resume() if not resume_result.cleared_to_resume(): - raise FoundationError( - "human-review reentry requires cleared resume authority" - ) + raise FoundationError("human-review reentry requires cleared resume authority") if resume_result.next_stage() is RunStage.HUMAN_REVIEW: - raise FoundationError( - "human-review reentry cannot resume to human_review" - ) + raise FoundationError("human-review reentry cannot resume to human_review") if resume_result.state.digest() != resume_operation.run_state.digest(): - raise FoundationError( - "human-review reentry resume state mismatch" - ) + raise FoundationError("human-review reentry resume state mismatch") def _status_from_loop( self, @@ -259,6 +248,5 @@ def _status_from_loop( return HumanReviewReentryStatus.STEP_LIMIT_REACHED raise FoundationError( - f"unsupported human-review reentry stop reason: " - f"{loop_result.stop_reason.value}" + f"unsupported human-review reentry stop reason: {loop_result.stop_reason.value}" ) diff --git a/src/ix_sally/human_review_reentry_audit.py b/src/ix_sally/human_review_reentry_audit.py index 62fc07f..f2333ed 100644 --- a/src/ix_sally/human_review_reentry_audit.py +++ b/src/ix_sally/human_review_reentry_audit.py @@ -2,17 +2,17 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass from enum import StrEnum -from typing import Iterable from ix_sally.digest import DigestRecord, JsonArray, JsonObject from ix_sally.foundation import CanonicalKey, FoundationError, require_text -from ix_sally.human_review_reentry import HumanReviewReentryStatus from ix_sally.human_review_reentry_audit_status import HumanReviewReentryAuditStatus from ix_sally.human_review_reentry_coordination import ( HumanReviewReentryCoordinationResult, ) +from ix_sally.human_review_reentry_status import HumanReviewReentryStatus from ix_sally.human_review_workflow import HumanReviewWorkflowStage from ix_sally.stage_readiness import RunStage @@ -133,15 +133,12 @@ def create( reentry_status=reentry_status, ) if status is not expected_status: - raise FoundationError( - "human-review reentry audit status does not match findings" - ) + raise FoundationError("human-review reentry audit status does not match findings") return cls( report_id=report_id or CanonicalKey.from_text( - f"human-review-reentry-audit-{coordination_digest.value[:16]}-" - f"{status.value}", + f"human-review-reentry-audit-{coordination_digest.value[:16]}-{status.value}", field_name="report_id", ), coordination_digest=coordination_digest, @@ -180,7 +177,8 @@ def from_coordination( status=status, findings=findings, ) - def blocking_findings(self) -> tuple[HumanReviewReentryAuditFinding, ...]: + + def blocking_findings(self) -> tuple[HumanReviewReentryAuditFinding, ...]: """Return blocking findings.""" return tuple(finding for finding in self.findings if finding.is_blocking()) @@ -355,7 +353,7 @@ def _findings_from_coordination( detail="Human-review reentry was not recorded in control-plane state.", ) ) - if coordination.recorded_reentry() and coordination.control_plane.reentry_count() > 0: + if coordination.recorded_reentry() and coordination.control_plane.reentry_count() > 0: findings.append( _finding( severity=HumanReviewReentryAuditSeverity.INFO, diff --git a/src/ix_sally/human_review_reentry_audit_ledger.py b/src/ix_sally/human_review_reentry_audit_ledger.py index a981041..5e866fa 100644 --- a/src/ix_sally/human_review_reentry_audit_ledger.py +++ b/src/ix_sally/human_review_reentry_audit_ledger.py @@ -2,19 +2,18 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass -from typing import TYPE_CHECKING, Iterable +from typing import TYPE_CHECKING from ix_sally.digest import DigestRecord, JsonArray, JsonObject from ix_sally.foundation import CanonicalKey, FoundationError from ix_sally.stage_readiness import RunStage if TYPE_CHECKING: - from ix_sally.human_review_reentry import HumanReviewReentryStatus - from ix_sally.human_review_reentry_audit import ( - HumanReviewReentryAuditReport, - HumanReviewReentryAuditStatus, - ) + from ix_sally.human_review_reentry_audit import HumanReviewReentryAuditReport + from ix_sally.human_review_reentry_audit_status import HumanReviewReentryAuditStatus + from ix_sally.human_review_reentry_status import HumanReviewReentryStatus @dataclass(frozen=True, slots=True) @@ -61,9 +60,7 @@ def create( ) -> HumanReviewReentryAuditLedgerEntry: """Create a normalized human-review reentry audit ledger entry.""" if sequence <= 0: - raise FoundationError( - "human-review reentry audit ledger sequence must be positive" - ) + raise FoundationError("human-review reentry audit ledger sequence must be positive") for field_name, value in { "finding_count": finding_count, @@ -73,17 +70,12 @@ def create( }.items(): if value < 0: raise FoundationError( - f"human-review reentry audit ledger {field_name} " - "must not be negative" + f"human-review reentry audit ledger {field_name} must not be negative" ) - if ( - blocking_finding_count + warning_finding_count + info_finding_count - != finding_count - ): + if blocking_finding_count + warning_finding_count + info_finding_count != finding_count: raise FoundationError( - "human-review reentry audit ledger finding subtotals must equal " - "finding_count" + "human-review reentry audit ledger finding subtotals must equal finding_count" ) audit_report_digest.require_algorithm("sha256") @@ -249,13 +241,11 @@ def create( for entry in normalized: if entry.sequence in seen_sequences: raise FoundationError( - f"duplicate human-review reentry audit ledger sequence: " - f"{entry.sequence}" + f"duplicate human-review reentry audit ledger sequence: {entry.sequence}" ) if entry.entry_id.value in seen_entry_ids: raise FoundationError( - f"duplicate human-review reentry audit ledger entry id: " - f"{entry.entry_id.value}" + f"duplicate human-review reentry audit ledger entry id: {entry.entry_id.value}" ) if entry.audit_report_digest.value in seen_report_digests: raise FoundationError( @@ -263,9 +253,7 @@ def create( f"{entry.audit_report_digest.value}" ) if entry.sequence <= previous_sequence: - raise FoundationError( - "human-review reentry audit ledger sequences must increase" - ) + raise FoundationError("human-review reentry audit ledger sequences must increase") seen_sequences.add(entry.sequence) seen_entry_ids.add(entry.entry_id.value) @@ -366,15 +354,11 @@ def to_payload(self) -> JsonObject: "waiting_entry_count": len(self.waiting_entries()), "blocking_entry_count": len(self.blocking_entries()), "warning_entry_count": len(self.warning_entries()), - "forge_dispatch_entry_count": len( - self.entries_for_stage(RunStage.FORGE_DISPATCH) - ), + "forge_dispatch_entry_count": len(self.entries_for_stage(RunStage.FORGE_DISPATCH)), "forge_result_processing_entry_count": len( self.entries_for_stage(RunStage.FORGE_RESULT_PROCESSING) ), - "advanced_reentry_entry_count": len( - self.entries_by_audit_status_value("passed") - ), + "advanced_reentry_entry_count": len(self.entries_by_audit_status_value("passed")), "waiting_reentry_entry_count": len( self.entries_by_audit_status_value("waiting_for_external_input") ), diff --git a/src/ix_sally/human_review_reentry_coordination.py b/src/ix_sally/human_review_reentry_coordination.py index 2e398b5..5eff378 100644 --- a/src/ix_sally/human_review_reentry_coordination.py +++ b/src/ix_sally/human_review_reentry_coordination.py @@ -7,14 +7,12 @@ from ix_sally.digest import DigestRecord, JsonObject from ix_sally.foundation import CanonicalKey, FoundationError from ix_sally.human_review_control_plane import HumanReviewControlPlaneState -from ix_sally.human_review_control_plane_report import ( - HumanReviewControlPlaneReportStatus, -) +from ix_sally.human_review_control_plane_report_status import HumanReviewControlPlaneReportStatus from ix_sally.human_review_reentry import ( HumanReviewReentryResult, HumanReviewReentryRunner, - HumanReviewReentryStatus, ) +from ix_sally.human_review_reentry_status import HumanReviewReentryStatus from ix_sally.human_review_workflow import ( HumanReviewWorkflowKit, HumanReviewWorkflowOperation, @@ -62,9 +60,7 @@ def create( ) -> HumanReviewReentryCoordinationReceipt: """Create a normalized human-review reentry coordination receipt.""" if max_steps <= 0: - raise FoundationError( - "human-review reentry coordination max_steps must be positive" - ) + raise FoundationError("human-review reentry coordination max_steps must be positive") if executed_steps < 0: raise FoundationError( "human-review reentry coordination executed_steps must not be negative" @@ -121,10 +117,7 @@ def recorded_reentry(self) -> bool: def waiting_for_external_input(self) -> bool: """Return whether reentry stopped waiting for outside input.""" - return ( - self.reentry_status - is HumanReviewReentryStatus.WAITING_FOR_EXTERNAL_INPUT - ) + return self.reentry_status is HumanReviewReentryStatus.WAITING_FOR_EXTERNAL_INPUT def to_payload(self) -> JsonObject: """Return a stable JSON-compatible reentry coordination receipt.""" @@ -198,21 +191,18 @@ def create( "human-review reentry coordination requires resume-recorded operation" ) if reentry_result.resume_operation.digest() != resume_operation.digest(): - raise FoundationError( - "human-review reentry coordination resume operation mismatch" - ) - if workflow_operation.receipt.workflow_stage is not HumanReviewWorkflowStage.REENTRY_RECORDED: + raise FoundationError("human-review reentry coordination resume operation mismatch") + if ( + workflow_operation.receipt.workflow_stage + is not HumanReviewWorkflowStage.REENTRY_RECORDED + ): raise FoundationError( "human-review reentry coordination requires reentry-recorded workflow" ) if workflow_operation.require_reentry().digest() != reentry_result.digest(): - raise FoundationError( - "human-review reentry coordination workflow reentry mismatch" - ) + raise FoundationError("human-review reentry coordination workflow reentry mismatch") if workflow_operation.run_state.digest() != reentry_result.state.digest(): - raise FoundationError( - "human-review reentry coordination workflow state mismatch" - ) + raise FoundationError("human-review reentry coordination workflow state mismatch") if receipt.resume_operation_digest != resume_operation.digest(): raise FoundationError( "human-review reentry coordination receipt resume digest mismatch" @@ -226,9 +216,7 @@ def create( "human-review reentry coordination receipt workflow digest mismatch" ) if receipt.after_state_digest != workflow_operation.run_state.digest(): - raise FoundationError( - "human-review reentry coordination receipt state digest mismatch" - ) + raise FoundationError("human-review reentry coordination receipt state digest mismatch") if receipt.after_control_plane_digest != workflow_operation.control_plane.digest(): raise FoundationError( "human-review reentry coordination receipt control-plane digest mismatch" diff --git a/src/ix_sally/human_review_reentry_ledger.py b/src/ix_sally/human_review_reentry_ledger.py index 31cf0e7..aca3e84 100644 --- a/src/ix_sally/human_review_reentry_ledger.py +++ b/src/ix_sally/human_review_reentry_ledger.py @@ -2,8 +2,9 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass -from typing import TYPE_CHECKING, Iterable +from typing import TYPE_CHECKING from ix_sally.digest import DigestRecord, JsonArray, JsonObject from ix_sally.foundation import CanonicalKey, FoundationError @@ -11,10 +12,8 @@ from ix_sally.stage_readiness import RunStage if TYPE_CHECKING: - from ix_sally.human_review_reentry import ( - HumanReviewReentryResult, - HumanReviewReentryStatus, - ) + from ix_sally.human_review_reentry import HumanReviewReentryResult + from ix_sally.human_review_reentry_status import HumanReviewReentryStatus @dataclass(frozen=True, slots=True) @@ -57,9 +56,7 @@ def create( if sequence <= 0: raise FoundationError("human-review reentry ledger sequence must be positive") if executed_steps < 0: - raise FoundationError( - "human-review reentry ledger executed_steps must not be negative" - ) + raise FoundationError("human-review reentry ledger executed_steps must not be negative") reentry_receipt_digest.require_algorithm("sha256") resume_operation_digest.require_algorithm("sha256") @@ -293,15 +290,11 @@ def to_payload(self) -> JsonObject: "changed_entry_count": len(self.changed_entries()), "external_input_entry_count": len(self.external_input_entries()), "advanced_entry_count": len(self.entries_by_status_value("advanced")), - "waiting_entry_count": len( - self.entries_by_status_value("waiting_for_external_input") - ), + "waiting_entry_count": len(self.entries_by_status_value("waiting_for_external_input")), "forge_result_processing_entry_count": len( self.entries_for_stage(RunStage.FORGE_RESULT_PROCESSING) ), - "chamber_close_entry_count": len( - self.entries_for_stage(RunStage.CHAMBER_CLOSE_READY) - ), + "chamber_close_entry_count": len(self.entries_for_stage(RunStage.CHAMBER_CLOSE_READY)), } def digest(self) -> DigestRecord: diff --git a/src/ix_sally/human_review_resolution.py b/src/ix_sally/human_review_resolution.py index 5ca7916..e37b281 100644 --- a/src/ix_sally/human_review_resolution.py +++ b/src/ix_sally/human_review_resolution.py @@ -246,8 +246,7 @@ def manual_investigation_count(self) -> int: return sum( 1 for resolution in self.resolutions - if resolution.status - is HumanReviewResolutionStatus.MANUAL_INVESTIGATION_REQUIRED + if resolution.status is HumanReviewResolutionStatus.MANUAL_INVESTIGATION_REQUIRED ) def blocker_acknowledgment_count(self) -> int: diff --git a/src/ix_sally/human_review_resume_coordination.py b/src/ix_sally/human_review_resume_coordination.py index 902cc22..8b481fb 100644 --- a/src/ix_sally/human_review_resume_coordination.py +++ b/src/ix_sally/human_review_resume_coordination.py @@ -53,9 +53,7 @@ def create( ) -> HumanReviewResumeCoordinationReceipt: """Create a normalized human-review resume coordination receipt.""" if resumed_stage is RunStage.HUMAN_REVIEW: - raise FoundationError( - "human-review resume coordination cannot resume to human_review" - ) + raise FoundationError("human-review resume coordination cannot resume to human_review") before_ledger_digest.require_algorithm("sha256") after_ledger_digest.require_algorithm("sha256") @@ -247,9 +245,7 @@ def certify_and_record( after_ledger = ledger.append_result(resume_result) entry = after_ledger.latest() if entry is None: - raise FoundationError( - "human-review resume coordination failed to append ledger entry" - ) + raise FoundationError("human-review resume coordination failed to append ledger entry") receipt = HumanReviewResumeCoordinationReceipt.from_coordination( before_ledger=ledger, diff --git a/src/ix_sally/human_review_resume_ledger.py b/src/ix_sally/human_review_resume_ledger.py index b5035de..c5b2088 100644 --- a/src/ix_sally/human_review_resume_ledger.py +++ b/src/ix_sally/human_review_resume_ledger.py @@ -2,8 +2,8 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass -from typing import Iterable from ix_sally.digest import DigestRecord, JsonArray, JsonObject from ix_sally.foundation import CanonicalKey, FoundationError, require_text @@ -178,8 +178,7 @@ def create( ) if entry.entry_id.value in seen_entry_ids: raise FoundationError( - f"duplicate human-review resume ledger entry id: " - f"{entry.entry_id.value}" + f"duplicate human-review resume ledger entry id: {entry.entry_id.value}" ) if entry.certificate_digest.value in seen_certificate_digests: raise FoundationError( @@ -187,9 +186,7 @@ def create( f"{entry.certificate_digest.value}" ) if entry.sequence <= previous_sequence: - raise FoundationError( - "human-review resume ledger sequences must increase" - ) + raise FoundationError("human-review resume ledger sequences must increase") seen_sequences.add(entry.sequence) seen_entry_ids.add(entry.entry_id.value) @@ -257,12 +254,8 @@ def to_payload(self) -> JsonObject: "execution_planning_resume_count": len( self.entries_for_stage(RunStage.EXECUTION_PLANNING) ), - "proposal_intake_resume_count": len( - self.entries_for_stage(RunStage.PROPOSAL_INTAKE) - ), - "chamber_close_resume_count": len( - self.entries_for_stage(RunStage.CHAMBER_CLOSE_READY) - ), + "proposal_intake_resume_count": len(self.entries_for_stage(RunStage.PROPOSAL_INTAKE)), + "chamber_close_resume_count": len(self.entries_for_stage(RunStage.CHAMBER_CLOSE_READY)), } def digest(self) -> DigestRecord: diff --git a/src/ix_sally/human_review_workflow.py b/src/ix_sally/human_review_workflow.py index 4a8eb9f..88a762c 100644 --- a/src/ix_sally/human_review_workflow.py +++ b/src/ix_sally/human_review_workflow.py @@ -349,8 +349,7 @@ def open_handoff( run_state: NinefoldRunState, control_plane: HumanReviewControlPlaneState | None = None, authority_note: str = ( - "Human authority is required before IX-Sally may treat these " - "targets as resolved." + "Human authority is required before IX-Sally may treat these targets as resolved." ), ) -> HumanReviewWorkflowOperation: """Record a human-review handoff and return the updated workflow operation.""" @@ -501,9 +500,7 @@ def record_reentry_audit( ) -> HumanReviewWorkflowOperation: """Record a human-review reentry audit and return updated workflow state.""" if run_state.digest() != audit_report.state_digest: - raise FoundationError( - "workflow reentry audit run state must match audit report" - ) + raise FoundationError("workflow reentry audit run state must match audit report") operation = self.coordinator.record_reentry_audit( audit_report=audit_report, @@ -584,9 +581,7 @@ def record_complete_reentry_closeout( ) -> HumanReviewWorkflowOperation: """Record a complete human-review reentry closeout report.""" if run_state.digest() != closeout_report.state_digest: - raise FoundationError( - "workflow complete reentry closeout run state must match report" - ) + raise FoundationError("workflow complete reentry closeout run state must match report") operation = self.coordinator.record_complete_reentry_closeout( closeout_report=closeout_report, diff --git a/src/ix_sally/judgments.py b/src/ix_sally/judgments.py index 15e85b9..a2b429b 100644 --- a/src/ix_sally/judgments.py +++ b/src/ix_sally/judgments.py @@ -2,9 +2,9 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass, field from enum import StrEnum -from typing import Iterable from ix_sally.agents import AgentRole from ix_sally.artifacts import AgentArtifact, AgentArtifactKind diff --git a/src/ix_sally/jurisdiction.py b/src/ix_sally/jurisdiction.py index 7680301..68c783b 100644 --- a/src/ix_sally/jurisdiction.py +++ b/src/ix_sally/jurisdiction.py @@ -61,9 +61,7 @@ def evaluate(self, *, role: AgentRole, authority: str) -> JurisdictionDecision: role=role, authority=requested, status=JurisdictionStatus.DENIED, - reason=( - f"role {role.value} is prohibited from authority {requested.value}" - ), + reason=(f"role {role.value} is prohibited from authority {requested.value}"), ) return JurisdictionDecision( diff --git a/src/ix_sally/language/__init__.py b/src/ix_sally/language/__init__.py index bfd6745..dbe73b9 100644 --- a/src/ix_sally/language/__init__.py +++ b/src/ix_sally/language/__init__.py @@ -94,10 +94,10 @@ "IXTypeChecker", "IXTypeContext", "IXTypeReport", - "IXValueType", "IXValidationContext", "IXValidationError", "IXValidationReport", + "IXValueType", "Keyword", "LanguageDiagnostic", "LanguageNode", diff --git a/src/ix_sally/language/ast.py b/src/ix_sally/language/ast.py index f5b6f67..ea6faac 100644 --- a/src/ix_sally/language/ast.py +++ b/src/ix_sally/language/ast.py @@ -125,9 +125,7 @@ class NameExpression(Expression): def __post_init__(self) -> None: """Reject names outside the IX identifier grammar.""" if not isinstance(self.name, str) or not _IDENTIFIER_PATTERN.fullmatch(self.name): - raise FoundationError( - "IX expression name must be an ASCII identifier" - ) + raise FoundationError("IX expression name must be an ASCII identifier") def to_payload(self) -> JsonObject: """Return a stable JSON-compatible name expression.""" @@ -163,7 +161,9 @@ def to_payload(self) -> JsonObject: "span": self.span.to_payload(), "expression": self.expression.to_payload(), } - @dataclass(frozen=True, slots=True) + + +@dataclass(frozen=True, slots=True) class UnaryExpression(Expression): """An IX unary operation over one operand.""" @@ -213,9 +213,7 @@ def __post_init__(self) -> None: field_name="binary right operand", ) if self.left.span.start > self.right.span.start: - raise FoundationError( - "IX binary expression operands must remain in source order" - ) + raise FoundationError("IX binary expression operands must remain in source order") def children(self) -> tuple[LanguageNode, ...]: """Return binary operands in source order.""" @@ -241,10 +239,6 @@ def _require_child_span( ) -> None: """Require one child span to belong to and fit inside its parent.""" if parent.filename != child.filename: - raise FoundationError( - f"IX {field_name} span must use the parent filename" - ) + raise FoundationError(f"IX {field_name} span must use the parent filename") if child.start < parent.start or child.end > parent.end: - raise FoundationError( - f"IX {field_name} span must be contained by the parent span" - ) + raise FoundationError(f"IX {field_name} span must be contained by the parent span") diff --git a/src/ix_sally/language/errors.py b/src/ix_sally/language/errors.py index 128672b..ede5f1b 100644 --- a/src/ix_sally/language/errors.py +++ b/src/ix_sally/language/errors.py @@ -77,12 +77,7 @@ def create( def format(self) -> str: """Return a stable one-line diagnostic rendering.""" - rendered = ( - f"{self.span.label()}: " - f"{self.severity.value} " - f"[{self.code.value}]: " - f"{self.message}" - ) + rendered = f"{self.span.label()}: {self.severity.value} [{self.code.value}]: {self.message}" if self.hint is not None: return f"{rendered} Hint: {self.hint}" @@ -102,9 +97,7 @@ def to_payload(self) -> JsonObject: def digest(self) -> DigestRecord: """Return a deterministic digest for this diagnostic.""" - return DigestRecord.from_payload( - self.to_payload() - ) + return DigestRecord.from_payload(self.to_payload()) class IXLanguageError(FoundationError): @@ -115,9 +108,7 @@ def __init__( diagnostic: LanguageDiagnostic, ) -> None: self.diagnostic = diagnostic - super().__init__( - diagnostic.format() - ) + super().__init__(diagnostic.format()) class IXSyntaxError(IXLanguageError): diff --git a/src/ix_sally/language/expression_parser.py b/src/ix_sally/language/expression_parser.py index 997ccd2..5893bb2 100644 --- a/src/ix_sally/language/expression_parser.py +++ b/src/ix_sally/language/expression_parser.py @@ -63,9 +63,7 @@ def __post_init__(self) -> None: filename = self.tokens[0].span.filename if any(token.span.filename != filename for token in self.tokens): - raise FoundationError( - "IX expression token stream must use one source filename" - ) + raise FoundationError("IX expression token stream must use one source filename") def parse(self) -> Expression: """Parse and return one complete typed IX expression.""" @@ -156,7 +154,7 @@ def _parse_primary(self) -> Expression: if token.kind is TokenKind.LEFT_PAREN: return self._parse_group() - self._raise_expected_expression(token) + return self._raise_expected_expression(token) def _parse_group(self) -> GroupExpression: """Parse one parenthesized expression including both delimiters.""" @@ -179,7 +177,8 @@ def _parse_group(self) -> GroupExpression: span=opening.span.merge(closing.span), expression=expression, ) - def _binary_operator(self, token: LanguageToken) -> BinaryOperator | None: + + def _binary_operator(self, token: LanguageToken) -> BinaryOperator | None: """Return the binary operator represented by ``token``.""" if token.kind is TokenKind.KEYWORD and token.keyword is not None: return _BINARY_OPERATORS.get(token.keyword) diff --git a/src/ix_sally/language/frontend.py b/src/ix_sally/language/frontend.py index 8072bf8..ea1a524 100644 --- a/src/ix_sally/language/frontend.py +++ b/src/ix_sally/language/frontend.py @@ -57,9 +57,7 @@ def type_context(self) -> IXTypeContext: def to_payload(self) -> JsonObject: """Return a stable JSON-compatible front-end context.""" local_types: JsonArray = [binding.to_payload() for binding in self.local_types] - memory_types: JsonArray = [ - binding.to_payload() for binding in self.memory_types - ] + memory_types: JsonArray = [binding.to_payload() for binding in self.memory_types] return { "local_types": local_types, "memory_types": memory_types, @@ -95,24 +93,17 @@ def __post_init__(self) -> None: filename = self.program.span.filename if any(token.span.filename != filename for token in self.tokens): - raise FoundationError( - "IX front-end tokens and program must use one source filename" - ) + raise FoundationError("IX front-end tokens and program must use one source filename") if self.tokens[-1].span.end.offset != self.source_length: - raise FoundationError( - "IX front-end EOF offset must match the source length" - ) + raise FoundationError("IX front-end EOF offset must match the source length") program_digest = self.program.digest() if self.validation_report.program_digest != program_digest: - raise FoundationError( - "IX front-end validation report does not match the program" - ) + raise FoundationError("IX front-end validation report does not match the program") if self.type_report.program_digest != program_digest: - raise FoundationError( - "IX front-end type report does not match the program" - ) - @property + raise FoundationError("IX front-end type report does not match the program") + + @property def filename(self) -> str: """Return the analyzed source filename.""" return self.program.span.filename @@ -162,9 +153,7 @@ def to_payload(self) -> JsonObject: } for token in self.tokens ] - diagnostics: JsonArray = [ - diagnostic.to_payload() for diagnostic in self.diagnostics() - ] + diagnostics: JsonArray = [diagnostic.to_payload() for diagnostic in self.diagnostics()] program_digest = self.program.digest() validation_digest = self.validation_report.digest() type_digest = self.type_report.digest() @@ -202,7 +191,9 @@ def to_payload(self) -> JsonObject: def digest(self) -> DigestRecord: """Return a deterministic digest for this front-end analysis.""" return DigestRecord.from_payload(self.to_payload()) - @dataclass(frozen=True, slots=True) + + +@dataclass(frozen=True, slots=True) class IXFrontendAnalyzer: """Run the complete non-executing IX source front end.""" @@ -217,12 +208,10 @@ def analyze( """Tokenize, parse, validate, and type-check one IX source document.""" tokens = tokenize_ix(source, filename=filename) program = IXStatementParser(tokens=tokens).parse() - validation_report = IXSemanticValidator( - context=self.context.validation_context() - ).validate(program) - type_report = IXTypeChecker( - context=self.context.type_context() - ).check(program) + validation_report = IXSemanticValidator(context=self.context.validation_context()).validate( + program + ) + type_report = IXTypeChecker(context=self.context.type_context()).check(program) return IXFrontendAnalysis( source_digest=DigestRecord.from_payload({"source": source}), source_length=len(source), diff --git a/src/ix_sally/language/lexer.py b/src/ix_sally/language/lexer.py index de329bb..b7b69a0 100644 --- a/src/ix_sally/language/lexer.py +++ b/src/ix_sally/language/lexer.py @@ -133,9 +133,7 @@ def _scan_punctuation_or_operator(self, character: str) -> bool: return True if character == "=": - self._emit( - TokenKind.EQUAL_EQUAL if self._match("=") else TokenKind.EQUAL - ) + self._emit(TokenKind.EQUAL_EQUAL if self._match("=") else TokenKind.EQUAL) return True if character == "!": @@ -147,18 +145,12 @@ def _scan_punctuation_or_operator(self, character: str) -> bool: message="Unexpected '!'; IX supports '!=' but not unary bang.", hint="Use the 'not' keyword for logical negation.", ) - if character == ">": - self._emit( - TokenKind.GREATER_EQUAL - if self._match("=") - else TokenKind.GREATER - ) + if character == ">": + self._emit(TokenKind.GREATER_EQUAL if self._match("=") else TokenKind.GREATER) return True if character == "<": - self._emit( - TokenKind.LESS_EQUAL if self._match("=") else TokenKind.LESS - ) + self._emit(TokenKind.LESS_EQUAL if self._match("=") else TokenKind.LESS) return True return False @@ -205,11 +197,7 @@ def _scan_number(self) -> None: self._advance() lexeme = self._lexeme() - literal: int | float - if token_kind is TokenKind.INTEGER: - literal = int(lexeme) - else: - literal = float(lexeme) + literal: int | float = int(lexeme) if token_kind is TokenKind.INTEGER else float(lexeme) self._tokens.append( LanguageToken( @@ -242,7 +230,7 @@ def _scan_string(self) -> None: self._raise_syntax( code="syntax.unterminated-string", message="String literal must close before the end of the line.", - hint='Add a closing double quote (\").', + hint='Add a closing double quote (").', ) if character == "\\": @@ -252,7 +240,7 @@ def _scan_string(self) -> None: self._raise_syntax( code="syntax.unterminated-string", message="String literal ends after an escape marker.", - hint='Add an escaped character and a closing double quote (\").', + hint='Add an escaped character and a closing double quote (").', ) escaped = self._advance() @@ -269,7 +257,7 @@ def _scan_string(self) -> None: severity=DiagnosticSeverity.ERROR, message=f"Unsupported string escape '\\{escaped}'.", span=span, - hint='Use one of: \", \\\\, \\n, \\r, or \\t.', + hint='Use one of: ", \\\\, \\n, \\r, or \\t.', ) ) value_parts.append(value) @@ -280,7 +268,7 @@ def _scan_string(self) -> None: self._raise_syntax( code="syntax.unterminated-string", message="String literal reaches the end of the source without closing.", - hint='Add a closing double quote (\").', + hint='Add a closing double quote (").', ) def _emit(self, kind: TokenKind) -> None: @@ -297,7 +285,7 @@ def _raise_syntax( self, *, code: str, - message: str, + message: str, hint: str | None = None, ) -> NoReturn: """Raise a structured syntax error for the current token range.""" @@ -371,11 +359,7 @@ def _is_ascii_digit(character: str) -> bool: def _is_identifier_start(character: str) -> bool: """Return whether ``character`` may begin an IX identifier.""" - return ( - character == "_" - or "a" <= character <= "z" - or "A" <= character <= "Z" - ) + return character == "_" or "a" <= character <= "z" or "A" <= character <= "Z" def _is_identifier_continue(character: str) -> bool: diff --git a/src/ix_sally/language/source.py b/src/ix_sally/language/source.py index b0dd1ff..5806d46 100644 --- a/src/ix_sally/language/source.py +++ b/src/ix_sally/language/source.py @@ -81,9 +81,7 @@ def __post_init__(self) -> None: ), ) if self.end < self.start: - raise FoundationError( - "source span end must not precede start" - ) + raise FoundationError("source span end must not precede start") @classmethod def point( @@ -126,11 +124,7 @@ def covering( def label(self) -> str: """Return a compact human-readable source label.""" - start = ( - f"{self.filename}:" - f"{self.start.line}:" - f"{self.start.column}" - ) + start = f"{self.filename}:{self.start.line}:{self.start.column}" if self.start == self.end: return start @@ -138,18 +132,12 @@ def label(self) -> str: if self.start.line == self.end.line: return f"{start}-{self.end.column}" - return ( - f"{start}-" - f"{self.end.line}:" - f"{self.end.column}" - ) + return f"{start}-{self.end.line}:{self.end.column}" def merge(self, other: SourceSpan) -> SourceSpan: """Return the smallest span containing this span and ``other``.""" if self.filename != other.filename: - raise FoundationError( - "cannot merge source spans from different files" - ) + raise FoundationError("cannot merge source spans from different files") return SourceSpan( filename=self.filename, @@ -175,6 +163,4 @@ def to_payload(self) -> JsonObject: def digest(self) -> DigestRecord: """Return a deterministic digest for this source span.""" - return DigestRecord.from_payload( - self.to_payload() - ) + return DigestRecord.from_payload(self.to_payload()) diff --git a/src/ix_sally/language/statement_parser.py b/src/ix_sally/language/statement_parser.py index e6c5185..6c5fe9c 100644 --- a/src/ix_sally/language/statement_parser.py +++ b/src/ix_sally/language/statement_parser.py @@ -82,18 +82,12 @@ def _parse_statement(self) -> Statement: """Parse one supported executable statement.""" token = self._peek() if token.kind is not TokenKind.KEYWORD or token.keyword is None: - rendered = ( - "end of source" - if token.kind is TokenKind.EOF - else repr(token.lexeme) - ) + rendered = "end of source" if token.kind is TokenKind.EOF else repr(token.lexeme) self._raise_syntax( token=token, code="syntax.expected-statement", message=f"Expected an executable IX statement, found {rendered}.", - hint=( - "Begin with let, remember, recall, print, reply, assert, or trace." - ), + hint=("Begin with let, remember, recall, print, reply, assert, or trace."), ) keyword = token.keyword @@ -108,7 +102,7 @@ def _parse_statement(self) -> Statement: if statement_type is not None: return self._parse_expression_statement(statement_type) - self._raise_syntax( + return self._raise_syntax( token=token, code="syntax.unsupported-statement", message=f"Unsupported executable IX statement keyword {token.lexeme!r}.", @@ -169,9 +163,7 @@ def _parse_recall(self) -> RecallStatement: def _parse_expression_statement( self, - statement_type: type[ - PrintStatement | ReplyStatement | AssertStatement | TraceStatement - ], + statement_type: type[PrintStatement | ReplyStatement | AssertStatement | TraceStatement], ) -> PrintStatement | ReplyStatement | AssertStatement | TraceStatement: """Parse one keyword followed by a typed expression.""" opening = self._advance() @@ -205,10 +197,9 @@ def _parse_expression_to_statement_end(self) -> Expression: end=terminator.span.start, ) ) - return IXExpressionParser( - tokens=(*expression_tokens, expression_eof) - ).parse() - def _require_statement_end(self, *, context: str) -> None: + return IXExpressionParser(tokens=(*expression_tokens, expression_eof)).parse() + + def _require_statement_end(self, *, context: str) -> None: """Require newline or EOF after a non-expression statement.""" token = self._peek() if token.kind in {TokenKind.NEWLINE, TokenKind.EOF}: @@ -232,7 +223,7 @@ def _consume( token = self._peek() if token.kind is kind: return self._advance() - self._raise_syntax( + return self._raise_syntax( token=token, code=code, message=message, diff --git a/src/ix_sally/language/statements.py b/src/ix_sally/language/statements.py index 369fd83..3af42ce 100644 --- a/src/ix_sally/language/statements.py +++ b/src/ix_sally/language/statements.py @@ -43,9 +43,7 @@ def children(self) -> tuple[LanguageNode, ...]: def to_payload(self) -> JsonObject: """Return a stable JSON-compatible program representation.""" - statements: JsonArray = [ - statement.to_payload() for statement in self.statements - ] + statements: JsonArray = [statement.to_payload() for statement in self.statements] return { "node_type": "program", "span": self.span.to_payload(), @@ -257,9 +255,7 @@ def _require_child_span( if parent.filename != child.filename: raise FoundationError(f"IX {field_name} span must use the parent filename") if child.start < parent.start or child.end > parent.end: - raise FoundationError( - f"IX {field_name} span must be contained by the parent span" - ) + raise FoundationError(f"IX {field_name} span must be contained by the parent span") def _require_ordered_children( diff --git a/src/ix_sally/language/tokens.py b/src/ix_sally/language/tokens.py index 42e891e..f002e12 100644 --- a/src/ix_sally/language/tokens.py +++ b/src/ix_sally/language/tokens.py @@ -98,14 +98,10 @@ def literal_value(self) -> bool | None: return False if self is Keyword.NULL: return None - raise FoundationError( - f"IX keyword {self.value!r} does not represent a literal value" - ) + raise FoundationError(f"IX keyword {self.value!r} does not represent a literal value") -KEYWORDS_BY_LEXEME: Final[dict[str, Keyword]] = { - keyword.value: keyword for keyword in Keyword -} +KEYWORDS_BY_LEXEME: Final[dict[str, Keyword]] = {keyword.value: keyword for keyword in Keyword} @dataclass(frozen=True, slots=True) @@ -214,9 +210,7 @@ def _require_keyword_consistency(self) -> None: if self.keyword is None: raise FoundationError("keyword token must carry a keyword") if self.lexeme != self.keyword.value: - raise FoundationError( - "keyword token lexeme must match its keyword value" - ) + raise FoundationError("keyword token lexeme must match its keyword value") def _require_literal_consistency(self) -> None: """Reject literal values that do not match their token kind.""" @@ -235,16 +229,15 @@ def _require_literal_consistency(self) -> None: raise FoundationError("float token literal must be a float") return - if self.kind is TokenKind.KEYWORD and self.keyword is not None: - if self.keyword.is_literal(): - expected = self.keyword.literal_value() - if self.literal is not expected: - raise FoundationError( - "literal keyword token carries an incorrect literal value" - ) - return + if ( + self.kind is TokenKind.KEYWORD + and self.keyword is not None + and self.keyword.is_literal() + ): + expected = self.keyword.literal_value() + if self.literal is not expected: + raise FoundationError("literal keyword token carries an incorrect literal value") + return if self.literal is not None: - raise FoundationError( - f"{self.kind.value} token must not carry a literal value" - ) + raise FoundationError(f"{self.kind.value} token must not carry a literal value") diff --git a/src/ix_sally/language/type_system.py b/src/ix_sally/language/type_system.py index 2fea0ea..fd83571 100644 --- a/src/ix_sally/language/type_system.py +++ b/src/ix_sally/language/type_system.py @@ -67,9 +67,7 @@ class IXTypeBinding: def __post_init__(self) -> None: """Require one valid IX identifier.""" - if not isinstance(self.name, str) or not _IDENTIFIER_PATTERN.fullmatch( - self.name - ): + if not isinstance(self.name, str) or not _IDENTIFIER_PATTERN.fullmatch(self.name): raise FoundationError("IX type binding name must be an ASCII identifier") def to_payload(self) -> JsonObject: @@ -153,13 +151,9 @@ def memory_type(self, name: str) -> IXValueType | None: def to_payload(self) -> JsonObject: """Return a stable JSON-compatible type report.""" - diagnostics: JsonArray = [ - diagnostic.to_payload() for diagnostic in self.diagnostics - ] + diagnostics: JsonArray = [diagnostic.to_payload() for diagnostic in self.diagnostics] local_types: JsonArray = [binding.to_payload() for binding in self.local_types] - memory_types: JsonArray = [ - binding.to_payload() for binding in self.memory_types - ] + memory_types: JsonArray = [binding.to_payload() for binding in self.memory_types] return { "program_digest": { "algorithm": self.program_digest.algorithm, @@ -168,7 +162,7 @@ def to_payload(self) -> JsonObject: "diagnostics": diagnostics, "diagnostic_count": len(self.diagnostics), "error_count": len(self.errors()), - "local_types": local_types, + "local_types": local_types, "memory_types": memory_types, "is_valid": self.is_valid(), } @@ -249,10 +243,7 @@ def _check_statement( LanguageDiagnostic.create( code="typing.assertion-not-boolean", severity=DiagnosticSeverity.ERROR, - message=( - "Assert expression must be Boolean, " - f"not {value_type.value}." - ), + message=(f"Assert expression must be Boolean, not {value_type.value}."), span=statement.expression.span, hint="Use a comparison or Boolean expression after 'assert'.", ) @@ -286,9 +277,7 @@ def infer_ix_expression_type( diagnostics: list[LanguageDiagnostic] = [] return _infer_expression_type( expression, - local_types=_binding_map( - _normalize_bindings(local_types, field_name="local_types") - ), + local_types=_binding_map(_normalize_bindings(local_types, field_name="local_types")), diagnostics=diagnostics, ) @@ -338,14 +327,13 @@ def _infer_expression_type( ) return _binary_result_type( expression, - left_type=left_type, + left_type=left_type, right_type=right_type, diagnostics=diagnostics, ) raise FoundationError( - f"unsupported IX expression type for static analysis: " - f"{type(expression).__name__}" + f"unsupported IX expression type for static analysis: {type(expression).__name__}" ) @@ -377,10 +365,7 @@ def _unary_result_type( _append_operator_diagnostic( expression=expression, diagnostics=diagnostics, - message=( - "Unary operator 'not' requires a Boolean operand, " - f"not {operand_type.value}." - ), + message=(f"Unary operator 'not' requires a Boolean operand, not {operand_type.value}."), ) return IXValueType.UNKNOWN @@ -508,7 +493,7 @@ def _addition_result_type( if left_type is right_type is IXValueType.STRING: return IXValueType.STRING return _invalid_binary_result( - expression, + expression, left_type=left_type, right_type=right_type, diagnostics=diagnostics, @@ -605,8 +590,7 @@ def _equality_compatible( """Return whether two types may be compared for equality.""" return ( left_type is right_type - or left_type.is_numeric() - and right_type.is_numeric() + or (left_type.is_numeric() and right_type.is_numeric()) or IXValueType.NULL in {left_type, right_type} ) @@ -617,10 +601,8 @@ def _ordered_compatible( ) -> bool: """Return whether two types support ordered comparison.""" return ( - left_type.is_numeric() - and right_type.is_numeric() - or left_type is right_type is IXValueType.STRING - ) + left_type.is_numeric() and right_type.is_numeric() + ) or left_type is right_type is IXValueType.STRING def _statement_expression(statement: Statement) -> Expression: @@ -631,8 +613,7 @@ def _statement_expression(statement: Statement) -> Expression: ): return statement.expression raise FoundationError( - f"unsupported IX statement type for static analysis: " - f"{type(statement).__name__}" + f"unsupported IX statement type for static analysis: {type(statement).__name__}" ) @@ -645,14 +626,11 @@ def _normalize_bindings( by_name: dict[str, IXTypeBinding] = {} for binding in bindings: if not isinstance(binding, IXTypeBinding): - raise FoundationError( - f"IX type context {field_name} must contain IXTypeBinding values" - ) + raise FoundationError(f"IX type context {field_name} must contain IXTypeBinding values") existing = by_name.get(binding.name) if existing is not None and existing.value_type is not binding.value_type: raise FoundationError( - f"IX type context {field_name} contains conflicting types for " - f"{binding.name!r}" + f"IX type context {field_name} contains conflicting types for {binding.name!r}" ) by_name[binding.name] = binding return tuple(by_name[name] for name in sorted(by_name)) @@ -667,10 +645,7 @@ def _bindings_from_map( values: dict[str, IXValueType], ) -> tuple[IXTypeBinding, ...]: """Return deterministic bindings from one mutable type environment.""" - return tuple( - IXTypeBinding(name=name, value_type=values[name]) - for name in sorted(values) - ) + return tuple(IXTypeBinding(name=name, value_type=values[name]) for name in sorted(values)) def _binding_type( diff --git a/src/ix_sally/language/validation.py b/src/ix_sally/language/validation.py index 2cec369..7c68fda 100644 --- a/src/ix_sally/language/validation.py +++ b/src/ix_sally/language/validation.py @@ -102,9 +102,7 @@ def require_valid(self) -> IXValidationReport: def to_payload(self) -> JsonObject: """Return a stable JSON-compatible validation report.""" - diagnostics: JsonArray = [ - diagnostic.to_payload() for diagnostic in self.diagnostics - ] + diagnostics: JsonArray = [diagnostic.to_payload() for diagnostic in self.diagnostics] return { "program_digest": { "algorithm": self.program_digest.algorithm, @@ -174,9 +172,7 @@ def _validate_statement( LanguageDiagnostic.create( code="validation.duplicate-binding", severity=DiagnosticSeverity.ERROR, - message=( - f"Local name {statement.name!r} is already defined." - ), + message=(f"Local name {statement.name!r} is already defined."), span=statement.span, hint="Choose a new local name instead of redefining it.", ) @@ -201,14 +197,9 @@ def _validate_statement( LanguageDiagnostic.create( code="validation.unknown-memory", severity=DiagnosticSeverity.ERROR, - message=( - f"Memory name {statement.name!r} is not available." - ), + message=(f"Memory name {statement.name!r} is not available."), span=statement.span, - hint=( - "Remember the name earlier or provide it in the " - "validation context." - ), + hint=("Remember the name earlier or provide it in the validation context."), ) ) return tuple(diagnostics) @@ -221,7 +212,9 @@ def _validate_statement( ) ) return tuple(diagnostics) - def validate_ix_program( + + +def validate_ix_program( program: Program, *, context: IXValidationContext | None = None, @@ -252,8 +245,7 @@ def _statement_expression(statement: Statement) -> Expression: ): return statement.expression raise FoundationError( - f"unsupported IX statement type for semantic validation: " - f"{type(statement).__name__}" + f"unsupported IX statement type for semantic validation: {type(statement).__name__}" ) @@ -287,8 +279,6 @@ def _normalize_names( normalized: set[str] = set() for name in names: if not isinstance(name, str) or not _IDENTIFIER_PATTERN.fullmatch(name): - raise FoundationError( - f"IX validation {field_name} must contain ASCII identifiers" - ) + raise FoundationError(f"IX validation {field_name} must contain ASCII identifiers") normalized.add(name) return tuple(sorted(normalized)) diff --git a/src/ix_sally/memory.py b/src/ix_sally/memory.py index d6bf62b..6b6de07 100644 --- a/src/ix_sally/memory.py +++ b/src/ix_sally/memory.py @@ -2,9 +2,9 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass, field from enum import StrEnum -from typing import Iterable from ix_sally.agents import AgentRole from ix_sally.digest import DigestRecord, JsonArray, JsonObject diff --git a/src/ix_sally/memory_decisions.py b/src/ix_sally/memory_decisions.py index 377b27a..1a581da 100644 --- a/src/ix_sally/memory_decisions.py +++ b/src/ix_sally/memory_decisions.py @@ -2,9 +2,9 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass, field from enum import StrEnum -from typing import Iterable from ix_sally.agents import AgentRole from ix_sally.artifacts import AgentArtifact, AgentArtifactKind @@ -80,11 +80,15 @@ def create( if resulting_status is MemoryStatus.VERIFIED and not normalized_evidence: raise FoundationError("verified memory decisions require evidence digests") - if resulting_status in { - MemoryStatus.CONTRADICTED, - MemoryStatus.QUARANTINED, - MemoryStatus.REJECTED, - } and normalized_boundary_note is None: + if ( + resulting_status + in { + MemoryStatus.CONTRADICTED, + MemoryStatus.QUARANTINED, + MemoryStatus.REJECTED, + } + and normalized_boundary_note is None + ): raise FoundationError("blocking memory decisions require a boundary note") return cls( diff --git a/src/ix_sally/orchestration.py b/src/ix_sally/orchestration.py index 91a90a6..100305c 100644 --- a/src/ix_sally/orchestration.py +++ b/src/ix_sally/orchestration.py @@ -2,12 +2,12 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass from enum import StrEnum -from typing import Iterable from ix_sally.authority_processing import AuthorityProcessor -from ix_sally.chamber_closing import ChamberCloseResult, ChamberCloser +from ix_sally.chamber_closing import ChamberCloser, ChamberCloseResult from ix_sally.digest import DigestRecord, JsonObject from ix_sally.evidence_support_processing import EvidenceSupportProcessor from ix_sally.execution_dispatch import ExecutionDispatcher @@ -127,36 +127,36 @@ def advance_once( ) if snapshot.stage is RunStage.AUTHORITY_PROCESSING: - result = self.authority_processor.process_all_proposed(state=state) + authority_result = self.authority_processor.process_all_proposed(state=state) return StageAdvanceResult( - state=result.state, + state=authority_result.state, before_snapshot=snapshot, gate_decision=decision, kind=StageAdvanceKind.AUTHORITY_PROCESSED, detail="Processed proposed bounded actions through authority gates.", - processor_digest=result.digest(), + processor_digest=authority_result.digest(), ) if snapshot.stage is RunStage.EXECUTION_PLANNING: - result = self.execution_planner.queue_all_authorized(state=state) + planning_result = self.execution_planner.queue_all_authorized(state=state) return StageAdvanceResult( - state=result.state, + state=planning_result.state, before_snapshot=snapshot, gate_decision=decision, kind=StageAdvanceKind.EXECUTION_PLANNED, detail="Queued authorized bounded actions for IX-Forge dispatch.", - processor_digest=result.digest(), + processor_digest=planning_result.digest(), ) if snapshot.stage is RunStage.FORGE_DISPATCH: - result = self.execution_dispatcher.dispatch_all_queued(state=state) + dispatch_result = self.execution_dispatcher.dispatch_all_queued(state=state) return StageAdvanceResult( - state=result.state, + state=dispatch_result.state, before_snapshot=snapshot, gate_decision=decision, kind=StageAdvanceKind.EXECUTION_DISPATCHED, detail="Dispatched queued execution items to the Forge boundary.", - processor_digest=result.digest(), + processor_digest=dispatch_result.digest(), ) if snapshot.stage is RunStage.FORGE_RESULT_PROCESSING: @@ -169,28 +169,28 @@ def advance_once( detail="Dispatched execution items are waiting for supplied Forge results.", ) - result = self.forge_result_processor.process_results( + forge_result = self.forge_result_processor.process_results( state=state, results=normalized_results, ) return StageAdvanceResult( - state=result.state, + state=forge_result.state, before_snapshot=snapshot, gate_decision=decision, kind=StageAdvanceKind.FORGE_RESULTS_PROCESSED, detail="Processed supplied Forge results into action state.", - processor_digest=result.digest(), + processor_digest=forge_result.digest(), ) if snapshot.stage is RunStage.EVIDENCE_SUPPORT_REVIEW: - result = self.evidence_support_processor.process_all_unreviewed(state=state) + evidence_result = self.evidence_support_processor.process_all_unreviewed(state=state) return StageAdvanceResult( - state=result.state, + state=evidence_result.state, before_snapshot=snapshot, gate_decision=decision, kind=StageAdvanceKind.EVIDENCE_SUPPORT_REVIEWED, detail="Reviewed unreviewed claims against recorded evidence.", - processor_digest=result.digest(), + processor_digest=evidence_result.digest(), ) if snapshot.stage is RunStage.HUMAN_REVIEW: diff --git a/src/ix_sally/orchestration_loop.py b/src/ix_sally/orchestration_loop.py index 3931935..d3adb16 100644 --- a/src/ix_sally/orchestration_loop.py +++ b/src/ix_sally/orchestration_loop.py @@ -2,9 +2,9 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass from enum import StrEnum -from typing import Iterable from ix_sally.digest import DigestRecord, JsonObject from ix_sally.forge_results import ForgeResultRecord @@ -60,6 +60,7 @@ def latest_kind(self) -> StageAdvanceKind | None: def to_payload(self) -> JsonObject: """Return a stable JSON-compatible loop result.""" latest = self.trace.latest() + latest_kind = self.latest_kind() return { "state_digest": self.state.digest().value, "trace_digest": self.trace.digest().value, @@ -70,7 +71,7 @@ def to_payload(self) -> JsonObject: "executed_steps": self.executed_steps(), "forge_results_consumed": self.forge_results_consumed, "latest_receipt_digest": latest.digest().value if latest is not None else None, - "latest_kind": self.latest_kind().value if self.latest_kind() is not None else None, + "latest_kind": latest_kind.value if latest_kind is not None else None, "stopped_for_external_input": self.stopped_for_external_input(), "stopped_for_step_limit": self.stopped_for_step_limit(), "attempted_chamber_close": self.attempted_chamber_close(), @@ -98,9 +99,7 @@ def run_until_stop( state: NinefoldRunState, max_steps: int, forge_results: Iterable[ForgeResultRecord] = (), - chamber_close_summary: str = ( - "IX-Sally chamber closed after staged work completed." - ), + chamber_close_summary: str = ("IX-Sally chamber closed after staged work completed."), ) -> StageLoopResult: """Advance legal stages until input, close-attempt, or step limit stops the loop.""" if max_steps <= 0: diff --git a/src/ix_sally/orchestration_receipts.py b/src/ix_sally/orchestration_receipts.py index bc6adf9..c72088b 100644 --- a/src/ix_sally/orchestration_receipts.py +++ b/src/ix_sally/orchestration_receipts.py @@ -2,8 +2,8 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass -from typing import Iterable from ix_sally.digest import DigestRecord, JsonArray, JsonObject from ix_sally.foundation import CanonicalKey, FoundationError, require_text diff --git a/src/ix_sally/predictions.py b/src/ix_sally/predictions.py index da87f7f..fd7473a 100644 --- a/src/ix_sally/predictions.py +++ b/src/ix_sally/predictions.py @@ -2,9 +2,9 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass from enum import StrEnum -from typing import Iterable from ix_sally.agents import AgentRole from ix_sally.artifacts import AgentArtifact, AgentArtifactKind @@ -64,7 +64,10 @@ def create( if status is not PredictionStatus.PENDING_OUTCOME and normalized_observed is None: raise FoundationError("resolved predictions require an observed outcome") - if status in {PredictionStatus.PARTIAL, PredictionStatus.MISSED} and normalized_delta is None: + if ( + status in {PredictionStatus.PARTIAL, PredictionStatus.MISSED} + and normalized_delta is None + ): raise FoundationError("partial or missed predictions require a delta note") return cls( diff --git a/src/ix_sally/proposal_gateway.py b/src/ix_sally/proposal_gateway.py index 9ab9102..60067a0 100644 --- a/src/ix_sally/proposal_gateway.py +++ b/src/ix_sally/proposal_gateway.py @@ -91,10 +91,7 @@ def from_intake( action_count=intake_result.action_count(), claim_count=intake_result.claim_count(), requires_human_review=intake_result.requires_human_review(), - detail=( - "Accepted Sally proposal into run state after proposal-intake " - "stage gate." - ), + detail=("Accepted Sally proposal into run state after proposal-intake stage gate."), ) def changed_state(self) -> bool: diff --git a/src/ix_sally/proposal_intake.py b/src/ix_sally/proposal_intake.py index 279a036..8942d90 100644 --- a/src/ix_sally/proposal_intake.py +++ b/src/ix_sally/proposal_intake.py @@ -29,7 +29,10 @@ def action_count(self) -> int: def claim_count(self) -> int: """Return the number of claims carried by the proposal.""" - return len(self.proposal_artifact.data.get("claims", [])) + claims = self.proposal_artifact.data.get("claims", []) + if isinstance(claims, list): + return len(claims) + return 0 def requires_human_review(self) -> bool: """Return whether any bounded action requires human boundary review.""" diff --git a/src/ix_sally/proposals.py b/src/ix_sally/proposals.py index d6fbccd..537691d 100644 --- a/src/ix_sally/proposals.py +++ b/src/ix_sally/proposals.py @@ -2,8 +2,8 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass, field -from typing import Iterable from ix_sally.agents import AgentRole from ix_sally.artifacts import AgentArtifact, AgentArtifactKind diff --git a/src/ix_sally/recording.py b/src/ix_sally/recording.py index fc1b0bf..ef2efd6 100644 --- a/src/ix_sally/recording.py +++ b/src/ix_sally/recording.py @@ -68,8 +68,7 @@ def record_evidence( event_type=RuntimeEventType.EVIDENCE_RECORDED, actor=evidence.produced_by, summary=( - f"Recorded evidence from {evidence.produced_by.value}: " - f"{evidence.status.value}." + f"Recorded evidence from {evidence.produced_by.value}: {evidence.status.value}." ), payload=event_payload_with_reference( reference_type="evidence-record", @@ -127,8 +126,7 @@ def record_action( event_type=RuntimeEventType.JURISDICTION_DECIDED, actor=action.proposed_by, summary=( - f"Recorded bounded action from {action.proposed_by.value}: " - f"{action.status.value}." + f"Recorded bounded action from {action.proposed_by.value}: {action.status.value}." ), payload=event_payload_with_reference( reference_type="bounded-action", @@ -157,8 +155,7 @@ def record_action_update( event_type=RuntimeEventType.JURISDICTION_DECIDED, actor=action.proposed_by, summary=( - f"Updated bounded action from {action.proposed_by.value}: " - f"{action.status.value}." + f"Updated bounded action from {action.proposed_by.value}: {action.status.value}." ), payload=event_payload_with_reference( reference_type="bounded-action", @@ -222,8 +219,7 @@ def replace_execution_queue_item( event_type=RuntimeEventType.AGENT_ARTIFACT_RECORDED, actor=item.dispatch_role, summary=( - f"Updated execution queue item for {item.dispatch_role.value}: " - f"{item.status.value}." + f"Updated execution queue item for {item.dispatch_role.value}: {item.status.value}." ), payload=event_payload_with_reference( reference_type="execution-queue-item", @@ -245,8 +241,7 @@ def record_forge_result( event_type=RuntimeEventType.AGENT_ARTIFACT_RECORDED, actor=result.executed_by, summary=( - f"Recorded Forge result from {result.executed_by.value}: " - f"{result.status.value}." + f"Recorded Forge result from {result.executed_by.value}: {result.status.value}." ), payload=event_payload_with_reference( reference_type="forge-result", diff --git a/src/ix_sally/runtime.py b/src/ix_sally/runtime.py index 02ff58e..c37bdb0 100644 --- a/src/ix_sally/runtime.py +++ b/src/ix_sally/runtime.py @@ -83,7 +83,9 @@ def opening_event(self, *, sequence: int = 1) -> RuntimeEvent: sequence=sequence, cycle=0, event_type=RuntimeEventType.CHAMBER_OPENED, - summary="IX-Sally chamber opened with doctrine, ninefold roles, and jurisdiction gates.", + summary=( + "IX-Sally chamber opened with doctrine, ninefold roles, and jurisdiction gates." + ), payload={ "contract_digest": self.chamber.contract.digest().value, "doctrine_digest": self.chamber.doctrine_catalog.digest().value, diff --git a/src/ix_sally/session_baseline.py b/src/ix_sally/session_baseline.py index 83c3666..af70723 100644 --- a/src/ix_sally/session_baseline.py +++ b/src/ix_sally/session_baseline.py @@ -67,8 +67,7 @@ def session_one_baseline_payload() -> JsonObject: "role": definition.role.value, "title": definition.title, "prohibited_authorities": [ - authority.value - for authority in definition.prohibited_authorities + authority.value for authority in definition.prohibited_authorities ], } ) @@ -80,12 +79,8 @@ def session_one_baseline_payload() -> JsonObject: "runtime": kit.to_payload(), "opening_event_type": opening_event.event_type.value, "opening_event_digest": opening_event.digest().value, - "doctrine_rule_count": len( - kit.chamber.doctrine_catalog.rules - ), - "role_count": len( - kit.role_registry.definitions - ), + "doctrine_rule_count": len(kit.chamber.doctrine_catalog.rules), + "role_count": len(kit.role_registry.definitions), "roles": roles_payload, "contract_mode": kit.chamber.contract.mode.value, "max_cycles": kit.chamber.contract.max_cycles, @@ -96,6 +91,4 @@ def session_one_baseline_payload() -> JsonObject: def session_one_baseline_digest() -> DigestRecord: """Return a deterministic digest for the session-one baseline payload.""" - return DigestRecord.from_payload( - session_one_baseline_payload() - ) + return DigestRecord.from_payload(session_one_baseline_payload()) diff --git a/src/ix_sally/stage_gate.py b/src/ix_sally/stage_gate.py index f5e3c7f..10d8ef0 100644 --- a/src/ix_sally/stage_gate.py +++ b/src/ix_sally/stage_gate.py @@ -43,8 +43,7 @@ def from_snapshot( else: status = StageGateStatus.BLOCKED rationale = ( - f"Stage gate expected {expected_stage.value} but observed " - f"{snapshot.stage.value}." + f"Stage gate expected {expected_stage.value} but observed {snapshot.stage.value}." ) return cls( diff --git a/src/ix_sally/stage_readiness.py b/src/ix_sally/stage_readiness.py index 7f35022..f7d0570 100644 --- a/src/ix_sally/stage_readiness.py +++ b/src/ix_sally/stage_readiness.py @@ -65,9 +65,7 @@ def from_state(cls, state: NinefoldRunState) -> RunStageCounts: finding.claim_digest.value for finding in state.evidence_support.findings } unreviewed_claims = sum( - 1 - for claim in state.claims.claims - if claim.digest().value not in reviewed_claim_digests + 1 for claim in state.claims.claims if claim.digest().value not in reviewed_claim_digests ) blocking_actions = state.blocked_action_count() diff --git a/src/ix_sally/state_audit.py b/src/ix_sally/state_audit.py index 1ee126e..b41375c 100644 --- a/src/ix_sally/state_audit.py +++ b/src/ix_sally/state_audit.py @@ -2,9 +2,9 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass from enum import StrEnum -from typing import Iterable from ix_sally.digest import DigestRecord, JsonArray, JsonObject from ix_sally.foundation import CanonicalKey, FoundationError, require_text @@ -114,17 +114,13 @@ def blocking_findings(self) -> tuple[StateAuditFinding, ...]: def warning_findings(self) -> tuple[StateAuditFinding, ...]: """Return warning findings.""" return tuple( - finding - for finding in self.findings - if finding.severity is StateAuditSeverity.WARNING + finding for finding in self.findings if finding.severity is StateAuditSeverity.WARNING ) def info_findings(self) -> tuple[StateAuditFinding, ...]: """Return informational findings.""" return tuple( - finding - for finding in self.findings - if finding.severity is StateAuditSeverity.INFO + finding for finding in self.findings if finding.severity is StateAuditSeverity.INFO ) def ready_for_close(self) -> bool: @@ -218,7 +214,9 @@ def _action_findings(self, state: NinefoldRunState) -> tuple[StateAuditFinding, StateAuditFinding.create( severity=StateAuditSeverity.BLOCKING, summary="Bounded actions block autonomous continuation.", - detail=f"{state.blocked_action_count()} bounded action(s) are denied or blocked.", + detail=( + f"{state.blocked_action_count()} bounded action(s) are denied or blocked." + ), reference="actions.blocked", ) ) diff --git a/src/ix_sally/transfer.py b/src/ix_sally/transfer.py index 6c88e2f..e56008d 100644 --- a/src/ix_sally/transfer.py +++ b/src/ix_sally/transfer.py @@ -2,9 +2,9 @@ from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass from enum import StrEnum -from typing import Iterable from ix_sally.agents import AgentRole from ix_sally.artifacts import AgentArtifact, AgentArtifactKind @@ -78,9 +78,8 @@ def create( if status is TransferStatus.PASSED and evidence_digest is None: raise FoundationError("passed transfer trials require an evidence digest") - if status in {TransferStatus.PARTIAL, TransferStatus.FAILED}: - if normalized_failure is None: - raise FoundationError("partial or failed transfer trials require a failure note") + if status in {TransferStatus.PARTIAL, TransferStatus.FAILED} and normalized_failure is None: + raise FoundationError("partial or failed transfer trials require a failure note") if status is TransferStatus.BLOCKED and normalized_boundary is None: raise FoundationError("blocked transfer trials require a boundary note") diff --git a/tests/cognition/__init__.py b/tests/cognition/__init__.py new file mode 100644 index 0000000..c6197e8 --- /dev/null +++ b/tests/cognition/__init__.py @@ -0,0 +1 @@ +"""Tests for the integrated IX-Sally cognitive architecture.""" diff --git a/tests/cognition/test_active_memory.py b/tests/cognition/test_active_memory.py new file mode 100644 index 0000000..23ebc18 --- /dev/null +++ b/tests/cognition/test_active_memory.py @@ -0,0 +1,127 @@ +"""Active multi-layer memory tests.""" + +from __future__ import annotations + +import pytest + +from ix_sally.cognition import ( + ActiveMemoryEntry, + ActiveMemoryStatus, + ActiveMemoryStore, + MemoryLayer, +) +from ix_sally.digest import DigestRecord +from ix_sally.foundation import FoundationError + + +def _verified(identifier: str, content: str, sequence: int) -> ActiveMemoryEntry: + return ActiveMemoryEntry.create( + memory_id=identifier, + layer=MemoryLayer.EPISODIC, + content=content, + confidence=0.9, + status=ActiveMemoryStatus.VERIFIED, + sequence=sequence, + evidence_digests=(DigestRecord.from_payload({"id": identifier}),), + tags=("test",), + ) + + +def test_verified_memory_requires_evidence() -> None: + """A memory cannot become verified through status assignment alone.""" + with pytest.raises(FoundationError, match="requires evidence"): + ActiveMemoryEntry.create( + memory_id="unsupported", + layer=MemoryLayer.SEMANTIC, + content="Unsupported memory.", + confidence=1.0, + status=ActiveMemoryStatus.VERIFIED, + sequence=1, + ) + + +def test_truth_only_retrieval_excludes_candidates() -> None: + """Truth retrieval must omit unverified candidates even when lexical match is high.""" + verified = _verified("verified-blue", "The sky appears blue during daytime.", 1) + candidate = ActiveMemoryEntry.create( + memory_id="candidate-blue", + layer=MemoryLayer.SEMANTIC, + content="The sky is always blue in every condition.", + confidence=1.0, + status=ActiveMemoryStatus.CANDIDATE, + sequence=2, + ) + store = ActiveMemoryStore((verified, candidate)) + + results = store.retrieve("sky blue", truth_only=True) + + assert tuple(result.entry.memory_id.value for result in results) == ("verified-blue",) + + +def test_retrieval_order_is_deterministic() -> None: + """Equivalent stores must rank retrieval results identically.""" + first = _verified("first", "red blue green", 1) + second = _verified("second", "red blue", 2) + store = ActiveMemoryStore((first, second)) + + one = store.retrieve("red blue", limit=2) + two = store.retrieve("red blue", limit=2) + + assert one == two + assert one[0].entry.memory_id.value == "second" + + +def test_memory_replacement_requires_later_sequence() -> None: + """Memory history must not move backward or overwrite at the same sequence.""" + store = ActiveMemoryStore((_verified("entry", "Original.", 3),)) + replacement = _verified("entry", "Replacement.", 3) + + with pytest.raises(FoundationError, match="sequence must increase"): + store.replace(replacement) + + +def test_consolidation_requires_verified_sources() -> None: + """Semantic consolidation must not launder candidate memory into truth.""" + candidate = ActiveMemoryEntry.create( + memory_id="candidate", + layer=MemoryLayer.EPISODIC, + content="Candidate episode.", + confidence=0.8, + status=ActiveMemoryStatus.CANDIDATE, + sequence=1, + ) + store = ActiveMemoryStore((candidate,)) + + with pytest.raises(FoundationError, match="verified source"): + store.consolidate( + memory_id="semantic", + source_ids=("candidate",), + content="Consolidated claim.", + evidence_digests=(DigestRecord.from_payload({"source": "candidate"}),), + confidence=0.8, + ) + + +def test_consolidation_creates_verified_semantic_memory() -> None: + """Verified source episodes may produce a provenance-linked semantic entry.""" + store = ActiveMemoryStore( + ( + _verified("episode-one", "The test passed once.", 1), + _verified("episode-two", "The test passed again.", 2), + ) + ) + updated = store.consolidate( + memory_id="repeated-success", + source_ids=("episode-one", "episode-two"), + content="The test passed in two observed episodes.", + evidence_digests=(DigestRecord.from_payload({"review": "accepted"}),), + confidence=0.9, + ) + + consolidated = updated.require("repeated-success") + assert consolidated.layer is MemoryLayer.SEMANTIC + assert consolidated.status is ActiveMemoryStatus.VERIFIED + assert tuple(source.value for source in consolidated.source_ids) == ( + "episode-one", + "episode-two", + ) diff --git a/tests/cognition/test_episodes_curriculum.py b/tests/cognition/test_episodes_curriculum.py new file mode 100644 index 0000000..0dbd700 --- /dev/null +++ b/tests/cognition/test_episodes_curriculum.py @@ -0,0 +1,197 @@ +"""Episode replay-chain and curriculum progression tests.""" + +from __future__ import annotations + +import pytest + +from ix_sally.cognition.curriculum import ( + Curriculum, + CurriculumLedger, + CurriculumSplit, + CurriculumTask, + CurriculumTrial, + TrialStatus, +) +from ix_sally.cognition.episodes import ( + CognitiveEpisode, + EpisodeLedger, + EpisodeStep, + EpisodeStepKind, + EpisodeStepStatus, +) +from ix_sally.digest import DigestRecord +from ix_sally.foundation import FoundationError + + +def _digest(value: str) -> DigestRecord: + return DigestRecord.from_payload({"value": value}) + + +def _episode( + sequence: int, + previous: DigestRecord | None, + *, + status: EpisodeStepStatus = EpisodeStepStatus.COMPLETED, +) -> CognitiveEpisode: + step = EpisodeStep.create( + index=0, + kind=EpisodeStepKind.PLANNING, + status=status, + detail="Bounded planning step.", + input_digests=(_digest(f"input-{sequence}"),), + output_digests=(_digest(f"output-{sequence}"),), + ) + return CognitiveEpisode.create( + episode_id=f"episode-{sequence}", + sequence=sequence, + task="Evaluate a bounded task.", + initial_state_digest=_digest(f"before-{sequence}"), + final_state_digest=_digest(f"after-{sequence}"), + steps=(step,), + previous_episode_digest=previous, + ) + + +def test_episode_ledger_requires_exact_chain_links() -> None: + """Each episode must bind to the digest of the prior episode.""" + first = _episode(0, None) + second = _episode(1, first.digest()) + + ledger = EpisodeLedger.create((first, second)) + + assert ledger.head_digest() == second.digest() + assert ledger.next_sequence() == 2 + + +def test_episode_ledger_rejects_wrong_previous_digest() -> None: + """A valid-looking episode must fail when its history link is wrong.""" + first = _episode(0, None) + second = _episode(1, _digest("wrong")) + + with pytest.raises(FoundationError, match="previous digest"): + EpisodeLedger.create((first, second)) + + +def test_episode_steps_require_contiguous_indexes() -> None: + """Replay order must not contain unrecorded gaps.""" + with pytest.raises(FoundationError, match="contiguous"): + CognitiveEpisode.create( + episode_id="broken", + sequence=0, + task="Broken episode.", + initial_state_digest=_digest("before"), + final_state_digest=_digest("after"), + steps=( + EpisodeStep.create( + index=1, + kind=EpisodeStepKind.INPUT, + status=EpisodeStepStatus.COMPLETED, + detail="Out of order.", + ), + ), + ) + + +def test_failed_episode_is_not_reported_complete() -> None: + """A failed step must remain visible at episode level.""" + episode = _episode(0, None, status=EpisodeStepStatus.FAILED) + + assert not episode.completed() + + +def _task( + task_id: str, + split: CurriculumSplit, + *, + prerequisites: tuple[str, ...] = (), + difficulty: int = 1, +) -> CurriculumTask: + return CurriculumTask.create( + task_id=task_id, + family="symbolic-reasoning", + description=f"Complete {task_id}.", + difficulty=difficulty, + split=split, + prerequisite_ids=prerequisites, + required_capabilities=("planning", "reasoning"), + pass_threshold=0.8, + ) + + +def _trial( + trial_id: str, + task_id: str, + sequence: int, + score: float, +) -> CurriculumTrial: + return CurriculumTrial.create( + trial_id=trial_id, + task_id=task_id, + sequence=sequence, + score=score, + status=TrialStatus.PASSED if score >= 0.8 else TrialStatus.PARTIAL, + evidence_digest=_digest(trial_id), + notes="Observed deterministic curriculum result.", + ) + + +def test_curriculum_eligibility_follows_prerequisites() -> None: + """Advanced tasks become eligible only after prerequisite evidence passes.""" + basic = _task("basic", CurriculumSplit.TRAINING) + advanced = _task( + "advanced", + CurriculumSplit.VALIDATION, + prerequisites=("basic",), + difficulty=2, + ) + ledger = CurriculumLedger(Curriculum.create((basic, advanced))) + + assert tuple(task.task_id.value for task in ledger.eligible_tasks()) == ("basic",) + + updated = ledger.record(_trial("basic-pass", "basic", 0, 0.9)) + + assert tuple(task.task_id.value for task in updated.eligible_tasks()) == ("advanced",) + + +def test_curriculum_reports_held_out_transfer_gap() -> None: + """Held-out performance must remain separate from validation performance.""" + tasks = Curriculum.create( + ( + _task("training", CurriculumSplit.TRAINING), + _task("validation", CurriculumSplit.VALIDATION), + _task("held-out", CurriculumSplit.HELD_OUT), + ) + ) + ledger = CurriculumLedger(tasks) + ledger = ledger.record(_trial("t1", "training", 0, 1.0)) + ledger = ledger.record(_trial("t2", "validation", 1, 0.9)) + ledger = ledger.record(_trial("t3", "held-out", 2, 0.7)) + + assert ledger.split_score(CurriculumSplit.TRAINING) == 1.0 + assert ledger.split_score(CurriculumSplit.VALIDATION) == 0.9 + assert ledger.split_score(CurriculumSplit.HELD_OUT) == 0.7 + assert ledger.transfer_gap() == 0.2 + + +def test_curriculum_rejects_unknown_prerequisite() -> None: + """Curriculum construction must fail on a missing dependency.""" + with pytest.raises(FoundationError, match="unknown prerequisites"): + Curriculum.create( + ( + _task( + "advanced", + CurriculumSplit.HELD_OUT, + prerequisites=("missing",), + ), + ) + ) + + +def test_curriculum_trial_sequence_is_append_only() -> None: + """Trial history must not accept skipped sequence numbers.""" + curriculum = Curriculum.create((_task("task", CurriculumSplit.TRAINING),)) + with pytest.raises(FoundationError, match="sequence must be contiguous"): + CurriculumLedger( + curriculum, + (_trial("late", "task", 1, 0.9),), + ) diff --git a/tests/cognition/test_executive_governance.py b/tests/cognition/test_executive_governance.py new file mode 100644 index 0000000..ee3d683 --- /dev/null +++ b/tests/cognition/test_executive_governance.py @@ -0,0 +1,255 @@ +"""Executive deliberation, cognitive governance bridge, and adaptation tests.""" + +from __future__ import annotations + +import pytest + +from ix_sally.claims import ClaimStatus +from ix_sally.cognition import ( + ActionSpec, + CognitiveValue, + CognitiveWorkspace, + FactEffect, + FactPattern, + FactStatus, + WorkspaceItem, + WorkspaceItemKind, + WorldFact, + WorldModel, +) +from ix_sally.cognition.active_memory import ActiveMemoryStore +from ix_sally.cognition.adaptation import AdaptationController, RegressionOutcome +from ix_sally.cognition.executive import ExecutiveController, ExecutiveDecisionStatus +from ix_sally.cognition.goals import GoalGraph, GoalSpec +from ix_sally.cognition.governance_bridge import CognitiveProposalBridge +from ix_sally.cognition.metacognition import CapabilityMeasure, ImprovementStatus, SelfModel +from ix_sally.cognition.uncertainty import CalibrationObservation, UncertaintyLedger +from ix_sally.digest import DigestRecord +from ix_sally.foundation import FoundationError + + +def _digest(name: str) -> DigestRecord: + return DigestRecord.from_payload({"evidence": name}) + + +def _pattern(value: str) -> FactPattern: + return FactPattern.create( + subject="machine", + predicate="state", + value=CognitiveValue.from_python(value), + ) + + +def _goal(*, risk_limit: float = 0.3, authority_required: bool = False) -> GoalSpec: + return GoalSpec.create( + goal_id="machine-ready", + description="Move the simulated machine to ready state.", + desired_state=_pattern("ready"), + priority=1.0, + utility=1.0, + risk_limit=risk_limit, + authority_required=authority_required, + ) + + +def _model() -> WorldModel: + return WorldModel( + ( + WorldFact.create( + fact_id="machine-off", + subject="machine", + predicate="state", + value=CognitiveValue.from_python("off"), + status=FactStatus.OBSERVED, + confidence=1.0, + evidence_digests=(_digest("machine-off"),), + ), + ), + (), + ) + + +def _action(*, risk: float = 0.1, authority_required: bool = False) -> ActionSpec: + return ActionSpec.create( + action_id="prepare-machine", + description="Prepare the simulated machine.", + preconditions=(_pattern("off"),), + effects=( + FactEffect.create( + subject="machine", + predicate="state", + value=CognitiveValue.from_python("ready"), + ), + ), + cost=1.0, + risk=risk, + authority_required=authority_required, + ) + + +def _controller_decision( + *, + goal: GoalSpec | None = None, + action: ActionSpec | None = None, + workspace: CognitiveWorkspace | None = None, + uncertainty: UncertaintyLedger | None = None, +): + calibration = uncertainty.report() if uncertainty is not None else None + return ExecutiveController().deliberate( + task="Prepare the machine.", + goals=GoalGraph.create((goal or _goal(),)), + workspace=workspace or CognitiveWorkspace(), + memory=ActiveMemoryStore(), + world_model=_model(), + actions=(action or _action(),), + calibration=calibration, + ) + + +def test_executive_returns_plan_ready_without_crossing_authority() -> None: + """A bounded low-risk plan may enter governance but is not executed.""" + decision = _controller_decision() + + assert decision.status is ExecutiveDecisionStatus.PLAN_READY + assert decision.plan is not None + assert decision.may_enter_governance() + + +def test_executive_requires_human_for_authority_action() -> None: + """Declared authority boundaries must survive planning.""" + decision = _controller_decision(action=_action(authority_required=True)) + + assert decision.status is ExecutiveDecisionStatus.REQUIRES_HUMAN + assert decision.blockers + + +def test_executive_blocks_plan_above_goal_risk_limit() -> None: + """A found plan must still fail a declared risk limit.""" + workspace = CognitiveWorkspace().admit( + WorkspaceItem.create( + item_id="risk", + kind=WorkspaceItemKind.RISK, + content="The simulated action exceeds the declared risk budget.", + confidence=1.0, + salience=1.0, + evidence_digests=(_digest("risk"),), + ) + ) + decision = _controller_decision( + goal=_goal(risk_limit=0.1), + action=_action(risk=0.5), + workspace=workspace, + ) + + assert decision.status is ExecutiveDecisionStatus.BLOCKED_RISK + assert not decision.may_enter_governance() + + +def test_executive_blocks_miscalibrated_confidence() -> None: + """Poor forecast calibration must be able to stop planning.""" + uncertainty = UncertaintyLedger.create( + ( + CalibrationObservation.create( + observation_id="overconfident", + capability_id="planning", + predicted_probability=1.0, + observed=False, + evidence_digest=_digest("calibration"), + context="Failed held-out planning trial.", + ), + ) + ) + + decision = _controller_decision(uncertainty=uncertainty) + + assert decision.status is ExecutiveDecisionStatus.BLOCKED_UNCERTAINTY + assert decision.plan is None + + +def test_governance_bridge_preserves_action_identity_and_human_boundary() -> None: + """Cognition must enter the existing proposal path rather than execute directly.""" + decision = _controller_decision(action=_action(authority_required=True)) + + result = CognitiveProposalBridge().bridge(decision=decision, cycle=3) + + assert result.proposal.cycle == 3 + assert result.proposal.proposed_actions[0].action_id.value == "prepare-machine" + assert result.proposal.proposed_actions[0].requires_human_boundary + assert result.proposal.claims[0].status is ClaimStatus.PARTIAL + assert result.receipt.executive_decision_digest == decision.digest() + + +def test_governance_bridge_rejects_blocked_decision() -> None: + """A blocked cognitive decision must not be converted into a proposal packet.""" + decision = _controller_decision(goal=_goal(risk_limit=0.0), action=_action(risk=0.5)) + + with pytest.raises(FoundationError, match="plan-ready"): + CognitiveProposalBridge().bridge(decision=decision, cycle=0) + + +def _measure(capability: str, score: float) -> CapabilityMeasure: + return CapabilityMeasure.create( + capability_id=capability, + score=score, + evidence_digests=(_digest(f"{capability}-{score}"),), + limitation="Measured only by the declared deterministic evaluation.", + ) + + +def test_adaptation_targets_weakest_measured_capability() -> None: + """Adaptation proposals must be evidence-driven and remain unapproved.""" + model = SelfModel((_measure("memory", 0.8), _measure("planning", 0.4))) + + proposal = AdaptationController().propose_for_weakest( + self_model=model, + description="Evaluate an alternate bounded planning heuristic.", + expected_benefit=0.2, + regression_risk=0.1, + supporting_evidence=(model.digest(),), + ) + + assert proposal.target_capability.value == "planning" + assert proposal.status is ImprovementStatus.PROPOSED + assert not proposal.may_enter_validation() + + +def test_regression_report_rejects_hidden_capability_loss() -> None: + """A target improvement must not hide a regression elsewhere.""" + before = SelfModel((_measure("memory", 0.8), _measure("planning", 0.4))) + after = SelfModel((_measure("memory", 0.5), _measure("planning", 0.7))) + controller = AdaptationController(permitted_regression=0.05) + proposal = controller.propose_for_weakest( + self_model=before, + description="Evaluate a planning strategy change.", + expected_benefit=0.3, + regression_risk=0.2, + supporting_evidence=(before.digest(),), + ) + + report = controller.compare(proposal=proposal, before=before, after=after) + + assert report.has_regression() + assert not report.may_request_validation() + assert any( + item.capability_id.value == "memory" and item.outcome is RegressionOutcome.REGRESSED + for item in report.findings + ) + + +def test_regression_report_requires_complete_measurement() -> None: + """Missing post-change measures must remain an incomplete result.""" + before = SelfModel((_measure("memory", 0.8), _measure("planning", 0.4))) + after = SelfModel((_measure("planning", 0.7),)) + controller = AdaptationController() + proposal = controller.propose_for_weakest( + self_model=before, + description="Evaluate a planning strategy change.", + expected_benefit=0.3, + regression_risk=0.2, + supporting_evidence=(before.digest(),), + ) + + report = controller.compare(proposal=proposal, before=before, after=after) + + assert not report.complete() + assert not report.may_request_validation() diff --git a/tests/cognition/test_goals_uncertainty.py b/tests/cognition/test_goals_uncertainty.py new file mode 100644 index 0000000..4c43087 --- /dev/null +++ b/tests/cognition/test_goals_uncertainty.py @@ -0,0 +1,209 @@ +"""Goal-graph and calibrated-uncertainty tests.""" + +from __future__ import annotations + +import pytest + +from ix_sally.cognition import CognitiveValue, FactPattern, FactStatus, WorldFact, WorldModel +from ix_sally.cognition.goals import GoalGraph, GoalSpec, GoalStatus +from ix_sally.cognition.uncertainty import CalibrationObservation, UncertaintyLedger +from ix_sally.digest import DigestRecord +from ix_sally.foundation import FoundationError + + +def _pattern(value: str) -> FactPattern: + return FactPattern.create( + subject="system", + predicate="state", + value=CognitiveValue.from_python(value), + ) + + +def _observed(value: str) -> WorldFact: + return WorldFact.create( + fact_id=f"state-{value}", + subject="system", + predicate="state", + value=CognitiveValue.from_python(value), + status=FactStatus.OBSERVED, + confidence=1.0, + evidence_digests=(DigestRecord.from_payload({"state": value}),), + ) + + +def _goal( + goal_id: str, + desired: str, + *, + priority: float, + dependencies: tuple[str, ...] = (), + status: GoalStatus = GoalStatus.PROPOSED, +) -> GoalSpec: + return GoalSpec.create( + goal_id=goal_id, + description=f"Reach {desired} state.", + desired_state=_pattern(desired), + priority=priority, + utility=0.8, + risk_limit=0.2, + dependency_ids=dependencies, + status=status, + ) + + +def test_goal_graph_selects_highest_priority_eligible_goal() -> None: + """Selection must respect dependencies before priority.""" + graph = GoalGraph.create( + ( + _goal("foundation", "prepared", priority=0.4), + _goal( + "advanced", + "complete", + priority=1.0, + dependencies=("foundation",), + ), + _goal("independent", "safe", priority=0.7), + ) + ) + + selected = graph.select(WorldModel()) + + assert selected is not None + assert selected.goal_id.value == "independent" + + +def test_goal_dependency_becomes_selectable_after_explicit_satisfaction() -> None: + """A dependency transition must change eligibility without changing priority.""" + graph = GoalGraph.create( + ( + _goal("foundation", "prepared", priority=0.4, status=GoalStatus.SATISFIED), + _goal( + "advanced", + "complete", + priority=1.0, + dependencies=("foundation",), + ), + ) + ) + + selected = graph.select(WorldModel()) + + assert selected is not None + assert selected.goal_id.value == "advanced" + + +def test_goal_graph_rejects_unknown_dependency() -> None: + """Missing prerequisite goals must never be silently ignored.""" + with pytest.raises(FoundationError, match="unknown dependencies"): + GoalGraph.create( + ( + _goal( + "advanced", + "complete", + priority=1.0, + dependencies=("missing",), + ), + ) + ) + + +def test_goal_graph_rejects_dependency_cycle() -> None: + """Recursive goals must fail before executive selection.""" + with pytest.raises(FoundationError, match="dependency cycle"): + GoalGraph.create( + ( + _goal("one", "one", priority=0.5, dependencies=("two",)), + _goal("two", "two", priority=0.5, dependencies=("one",)), + ) + ) + + +def test_goal_reconciliation_requires_world_state_match() -> None: + """A goal becomes satisfied only from an exact world-model fact.""" + graph = GoalGraph.create((_goal("ready", "ready", priority=1.0),)) + model = WorldModel((_observed("ready"),), ()) + + reconciled = graph.reconcile(model) + + assert reconciled.require("ready").status is GoalStatus.SATISFIED + assert reconciled.select(model) is None + + +def test_blocked_goal_requires_reason() -> None: + """Lifecycle blocking must retain an explicit explanation.""" + with pytest.raises(FoundationError, match="status reason"): + GoalSpec.create( + goal_id="blocked", + description="Blocked goal.", + desired_state=_pattern("blocked"), + priority=0.5, + utility=0.5, + risk_limit=0.5, + status=GoalStatus.BLOCKED, + ) + + +def _observation( + observation_id: str, + probability: float, + observed: bool, + *, + capability: str = "planning", +) -> CalibrationObservation: + return CalibrationObservation.create( + observation_id=observation_id, + capability_id=capability, + predicted_probability=probability, + observed=observed, + evidence_digest=DigestRecord.from_payload( + {"observation": observation_id, "observed": observed} + ), + context="Held-out deterministic evaluation.", + ) + + +def test_uncertainty_report_calculates_exact_brier_score() -> None: + """Forecast quality must be measured from original probabilities.""" + ledger = UncertaintyLedger.create( + ( + _observation("one", 0.8, True), + _observation("two", 0.2, False), + ) + ) + + report = ledger.report(bin_count=5) + + assert report.observation_count == 2 + assert report.brier_score == 0.04 + assert report.expected_calibration_error == 0.2 + + +def test_uncertainty_report_can_filter_capability() -> None: + """Calibration for one capability must exclude unrelated forecasts.""" + ledger = UncertaintyLedger.create( + ( + _observation("planning", 0.9, True), + _observation("memory", 0.1, True, capability="memory"), + ) + ) + + report = ledger.report(capability_id="planning") + + assert report.observation_count == 1 + assert report.brier_score == 0.01 + + +def test_uncertainty_ledger_rejects_duplicate_observation() -> None: + """Repeated identifiers must not overwrite calibration evidence.""" + observation = _observation("same", 0.5, True) + with pytest.raises(FoundationError, match="duplicate observations"): + UncertaintyLedger((observation, observation)) + + +def test_empty_calibration_report_is_explicit_zero_sample() -> None: + """No data must remain distinguishable from perfect calibration.""" + report = UncertaintyLedger().report() + + assert report.observation_count == 0 + assert report.brier_score == 0.0 + assert report.bins == () diff --git a/tests/cognition/test_learning_metacognition.py b/tests/cognition/test_learning_metacognition.py new file mode 100644 index 0000000..923b39f --- /dev/null +++ b/tests/cognition/test_learning_metacognition.py @@ -0,0 +1,156 @@ +"""Learning, transfer, self-model, and improvement-boundary tests.""" + +from __future__ import annotations + +import pytest + +from ix_sally.cognition import ( + CapabilityMeasure, + ImprovementProposal, + ImprovementStatus, + LearningLedger, + LearningOutcome, + OutcomeStatus, + SelfModel, + TransferEvaluation, +) +from ix_sally.digest import DigestRecord +from ix_sally.foundation import FoundationError + + +def _outcome(identifier: str, score: float) -> LearningOutcome: + return LearningOutcome.create( + outcome_id=identifier, + skill_id="planning", + task_family="novel-state", + status=OutcomeStatus.SUCCESS if score >= 0.8 else OutcomeStatus.PARTIAL, + score=score, + evidence_digest=DigestRecord.from_payload({"outcome": identifier, "score": score}), + notes="Observed deterministic task result.", + ) + + +def test_learning_ledger_updates_profile_from_observed_outcomes() -> None: + """Skill estimates must derive from actual recorded outcomes.""" + ledger = LearningLedger().record(_outcome("one", 0.5)).record(_outcome("two", 1.0)) + profile = ledger.require_profile("planning") + + assert profile.attempts == 2 + assert profile.successes == 1 + assert profile.mean_score == 0.75 + assert profile.last_outcome_digest == ledger.outcomes[-1].digest() + + +def test_learning_rejects_duplicate_outcome_identity() -> None: + """An outcome must not be counted twice.""" + outcome = _outcome("duplicate", 1.0) + ledger = LearningLedger().record(outcome) + + with pytest.raises(FoundationError, match="already exists"): + ledger.record(outcome) + + +def test_retention_score_uses_declared_recent_window() -> None: + """Retention must be transparent rather than an opaque self-rating.""" + ledger = LearningLedger() + for index, score in enumerate((0.2, 0.4, 0.8, 1.0), start=1): + ledger = ledger.record(_outcome(f"outcome-{index}", score)) + + assert ledger.retention_score("planning", recent_window=2) == 0.9 + + +def test_transfer_evaluation_exposes_generalization_gap() -> None: + """Transfer must compare familiar and held-out performance explicitly.""" + transfer = TransferEvaluation.create( + skill_id="planning", + familiar_score=0.95, + novel_score=0.8, + retention_score=0.85, + evidence_digests=(DigestRecord.from_payload({"suite": "transfer"}),), + ) + + assert transfer.generalization_gap() == 0.15 + assert transfer.passes() + + +def test_self_model_requires_evidence_and_limits() -> None: + """Capability scores must have evidence and an explicit limitation.""" + with pytest.raises(FoundationError, match="requires evidence"): + CapabilityMeasure.create( + capability_id="planning", + score=0.8, + evidence_digests=(), + limitation="Only evaluated on a bounded local suite.", + ) + + +def test_self_model_reports_weakest_measure() -> None: + """Metacognition must expose weakest measured capability deterministically.""" + evidence = DigestRecord.from_payload({"suite": "capabilities"}) + model = ( + SelfModel() + .update( + CapabilityMeasure.create( + capability_id="memory", + score=0.9, + evidence_digests=(evidence,), + limitation="Local deterministic retrieval only.", + ) + ) + .update( + CapabilityMeasure.create( + capability_id="planning", + score=0.6, + evidence_digests=(evidence,), + limitation="Exact-state planning only.", + ) + ) + ) + + weakest = model.weakest() + assert weakest is not None + assert weakest.capability_id.value == "planning" + + +def test_improvement_proposal_cannot_self_approve() -> None: + """A proposed change must remain blocked until a human decision digest exists.""" + proposal = ImprovementProposal.create( + proposal_id="improve-planning", + target_capability="planning", + description="Expand bounded planning evaluation coverage.", + expected_benefit=0.2, + regression_risk=0.1, + evidence_digests=(DigestRecord.from_payload({"gap": "planning"}),), + ) + + assert proposal.status is ImprovementStatus.PROPOSED + assert not proposal.may_enter_validation() + with pytest.raises(FoundationError, match="human decision digest"): + ImprovementProposal.create( + proposal_id="fake-approved", + target_capability="planning", + description="Invalid self-approved proposal.", + expected_benefit=0.2, + regression_risk=0.1, + evidence_digests=(DigestRecord.from_payload({"gap": "planning"}),), + status=ImprovementStatus.HUMAN_APPROVED, + ) + + +def test_human_decision_enables_validation_but_not_automatic_application() -> None: + """Human approval permits validation; it does not execute a code change.""" + proposal = ImprovementProposal.create( + proposal_id="improve-memory", + target_capability="memory", + description="Evaluate a revised retrieval scoring policy.", + expected_benefit=0.1, + regression_risk=0.1, + evidence_digests=(DigestRecord.from_payload({"gap": "memory"}),), + ) + approved = proposal.with_human_decision( + approved=True, + decision_digest=DigestRecord.from_payload({"human": "approved"}), + ) + + assert approved.status is ImprovementStatus.HUMAN_APPROVED + assert approved.may_enter_validation() diff --git a/tests/cognition/test_primitives_workspace.py b/tests/cognition/test_primitives_workspace.py new file mode 100644 index 0000000..42a7346 --- /dev/null +++ b/tests/cognition/test_primitives_workspace.py @@ -0,0 +1,164 @@ +"""Grounded primitive and cognitive workspace tests.""" + +from __future__ import annotations + +import pytest + +from ix_sally.cognition import ( + CognitiveValue, + CognitiveWorkspace, + PrimitiveExecutor, + PrimitiveKind, + PrimitiveOperation, + PrimitiveRegistry, + PrimitiveSpec, + PrimitiveStatus, + WorkspaceItem, + WorkspaceItemKind, + WorkspaceItemStatus, + default_primitive_registry, +) +from ix_sally.digest import DigestRecord +from ix_sally.foundation import FoundationError + + +def test_default_primitive_registry_executes_validated_operations() -> None: + """Built-in primitives must be real closed operations, not dynamic placeholders.""" + execution = PrimitiveExecutor(default_primitive_registry()).execute( + "multiply-two", + (CognitiveValue.from_python(6), CognitiveValue.from_python(7)), + ) + + assert execution.output == CognitiveValue.from_python(42) + assert execution.digest().algorithm == "sha256" + + +def test_candidate_primitive_cannot_execute() -> None: + """Unvalidated primitive candidates must fail closed.""" + candidate = PrimitiveSpec.create( + primitive_id="candidate-identity", + kind=PrimitiveKind.TRANSFORM, + operation=PrimitiveOperation.IDENTITY, + arity=1, + status=PrimitiveStatus.CANDIDATE, + description="Candidate identity operation.", + ) + executor = PrimitiveExecutor(PrimitiveRegistry.create((candidate,))) + + with pytest.raises(FoundationError, match="not validated"): + executor.execute("candidate-identity", (CognitiveValue.from_python(1),)) + + +def test_validated_primitive_requires_grounding_and_validation_evidence() -> None: + """Lifecycle state alone must not create executable authority.""" + with pytest.raises(FoundationError, match="grounding evidence"): + PrimitiveSpec.create( + primitive_id="invalid", + kind=PrimitiveKind.TRANSFORM, + operation=PrimitiveOperation.IDENTITY, + arity=1, + status=PrimitiveStatus.VALIDATED, + description="Invalid evidence-free primitive.", + ) + + +def test_primitive_arity_is_enforced() -> None: + """Primitive execution must reject missing operands.""" + with pytest.raises(FoundationError, match="requires 2 inputs"): + PrimitiveExecutor(default_primitive_registry()).execute( + "add-two", + (CognitiveValue.from_python(1),), + ) + + +def test_workspace_focus_prioritizes_risk_and_goal_salience() -> None: + """Attention selection must be deterministic and visible.""" + workspace = CognitiveWorkspace(capacity=3) + belief = WorkspaceItem.create( + item_id="belief", + kind=WorkspaceItemKind.BELIEF, + content="A low-priority belief.", + confidence=0.9, + salience=0.2, + ) + goal = WorkspaceItem.create( + item_id="goal", + kind=WorkspaceItemKind.GOAL, + content="A high-priority goal.", + confidence=0.8, + salience=0.9, + ) + risk = WorkspaceItem.create( + item_id="risk", + kind=WorkspaceItemKind.RISK, + content="A high-priority risk.", + confidence=0.7, + salience=1.0, + ) + for item in (belief, goal, risk): + workspace = workspace.admit(item) + + assert tuple(item.item_id.value for item in workspace.focus()) == ( + "risk", + "goal", + "belief", + ) + + +def test_workspace_evicts_lowest_attention_on_overflow() -> None: + """Capacity overflow must evict deterministically rather than grow unbounded.""" + workspace = CognitiveWorkspace(capacity=2) + for identifier, kind, salience in ( + ("low", WorkspaceItemKind.BELIEF, 0.1), + ("goal", WorkspaceItemKind.GOAL, 0.8), + ("risk", WorkspaceItemKind.RISK, 1.0), + ): + workspace = workspace.admit( + WorkspaceItem.create( + item_id=identifier, + kind=kind, + content=f"Workspace item {identifier}.", + confidence=0.8, + salience=salience, + ) + ) + + assert {item.item_id.value for item in workspace.items} == {"goal", "risk"} + + +def test_observation_requires_full_confidence() -> None: + """Direct observations must not be stored as partly observed facts.""" + with pytest.raises(FoundationError, match="observation confidence"): + WorkspaceItem.create( + item_id="weak-observation", + kind=WorkspaceItemKind.OBSERVATION, + content="A weakly stated observation.", + confidence=0.5, + salience=0.5, + ) + + +def test_workspace_replacement_preserves_capacity_and_identity() -> None: + """Updating one item must not disturb unrelated workspace entries.""" + original = WorkspaceItem.create( + item_id="goal", + kind=WorkspaceItemKind.GOAL, + content="Original goal.", + confidence=0.8, + salience=0.8, + ) + workspace = CognitiveWorkspace((original,), capacity=2) + replaced = WorkspaceItem.create( + item_id="goal", + kind=WorkspaceItemKind.GOAL, + content="Completed goal.", + confidence=1.0, + salience=0.2, + status=WorkspaceItemStatus.SATISFIED, + evidence_digests=(DigestRecord.from_payload({"result": "complete"}),), + ) + + updated = workspace.replace(replaced) + + assert updated.items == (replaced,) + assert updated.capacity == 2 diff --git a/tests/cognition/test_storage_restore_complete.py b/tests/cognition/test_storage_restore_complete.py new file mode 100644 index 0000000..1fb3520 --- /dev/null +++ b/tests/cognition/test_storage_restore_complete.py @@ -0,0 +1,185 @@ +"""Atomic snapshot storage and complete cognitive-state restoration tests.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from ix_sally.cognition import ( + ActionSpec, + CognitiveValue, + FactEffect, + FactPattern, + SallyCognitiveSystem, +) +from ix_sally.cognition.curriculum import ( + Curriculum, + CurriculumLedger, + CurriculumSplit, + CurriculumTask, + CurriculumTrial, + TrialStatus, +) +from ix_sally.cognition.episodes import ( + CognitiveEpisode, + EpisodeStep, + EpisodeStepKind, + EpisodeStepStatus, +) +from ix_sally.cognition.goals import GoalSpec +from ix_sally.cognition.storage import SnapshotRepository, SnapshotSource +from ix_sally.cognition.uncertainty import CalibrationObservation +from ix_sally.digest import DigestRecord +from ix_sally.foundation import FoundationError + + +def _digest(name: str) -> DigestRecord: + return DigestRecord.from_payload({"evidence": name}) + + +def _pattern(value: str) -> FactPattern: + return FactPattern.create( + subject="system", + predicate="state", + value=CognitiveValue.from_python(value), + ) + + +def _populated_system() -> SallyCognitiveSystem: + system = SallyCognitiveSystem.create() + system.execute_ix("remember answer = 40 + 2\n", filename="snapshot.ix") + system.register_action( + ActionSpec.create( + action_id="ready-system", + description="Move the simulated system to ready state.", + preconditions=(), + effects=( + FactEffect.create( + subject="system", + predicate="state", + value=CognitiveValue.from_python("ready"), + ), + ), + cost=1.0, + risk=0.1, + ) + ) + system.register_goal( + GoalSpec.create( + goal_id="ready-system", + description="Reach ready state.", + desired_state=_pattern("ready"), + priority=1.0, + utility=1.0, + risk_limit=0.2, + ) + ) + system.record_calibration( + CalibrationObservation.create( + observation_id="planning-calibration", + capability_id="planning", + predicted_probability=0.8, + observed=True, + evidence_digest=_digest("planning-calibration"), + context="Observed deterministic planning result.", + ) + ) + task = CurriculumTask.create( + task_id="basic-planning", + family="planning", + description="Complete a basic planning task.", + difficulty=1, + split=CurriculumSplit.TRAINING, + required_capabilities=("planning",), + ) + system.set_curriculum(CurriculumLedger(Curriculum.create((task,)))) + system.record_curriculum_trial( + CurriculumTrial.create( + trial_id="basic-planning-trial", + task_id="basic-planning", + sequence=0, + score=0.9, + status=TrialStatus.PASSED, + evidence_digest=_digest("basic-planning-trial"), + notes="Observed successful trial.", + ) + ) + before = system.digest() + step = EpisodeStep.create( + index=0, + kind=EpisodeStepKind.PLANNING, + status=EpisodeStepStatus.COMPLETED, + detail="Produced a bounded plan.", + input_digests=(before,), + output_digests=(_digest("plan"),), + ) + episode = CognitiveEpisode.create( + episode_id="episode-zero", + sequence=0, + task="Plan ready state.", + initial_state_digest=before, + final_state_digest=system.digest(), + steps=(step,), + ) + system.append_episode(episode) + return system + + +def test_complete_snapshot_restores_every_extended_subsystem() -> None: + """Snapshot restoration must reproduce the full canonical state exactly.""" + system = _populated_system() + snapshot = system.snapshot() + + restored = SallyCognitiveSystem.from_snapshot(snapshot) + + assert restored.state_payload() == system.state_payload() + assert restored.digest() == system.digest() + assert restored.runtime_memories["answer"] == CognitiveValue.from_python(42) + assert restored.goals.require("ready-system").description == "Reach ready state." + assert restored.curriculum is not None + assert len(restored.episodes.episodes) == 1 + + +def test_snapshot_repository_writes_and_loads_primary(tmp_path: Path) -> None: + """A persisted snapshot must verify before the save receipt succeeds.""" + system = _populated_system() + repository = SnapshotRepository(tmp_path / "state.json") + + receipt = repository.save(system.snapshot()) + loaded = repository.load() + + assert receipt.bytes_written > 0 + assert loaded.source is SnapshotSource.PRIMARY + assert loaded.snapshot.state_digest == system.digest() + + +def test_snapshot_repository_recovers_last_valid_backup(tmp_path: Path) -> None: + """A corrupted primary must not hide the separately validated backup.""" + repository = SnapshotRepository(tmp_path / "state.json") + first = _populated_system() + repository.save(first.snapshot()) + second = _populated_system() + second.execute_ix("remember second = 2\n", filename="second.ix") + second_snapshot = second.snapshot() + receipt = repository.save(second_snapshot) + assert receipt.backup_created + repository.path.write_text("{broken", encoding="utf-8") + + loaded = repository.load() + + assert loaded.source is SnapshotSource.BACKUP + assert loaded.snapshot.state_digest == first.snapshot().state_digest + assert loaded.primary_error is not None + + +def test_snapshot_repository_fails_when_primary_and_backup_are_invalid( + tmp_path: Path, +) -> None: + """Recovery must fail closed when no valid state remains.""" + repository = SnapshotRepository(tmp_path / "state.json") + repository.path.write_text("bad", encoding="utf-8") + repository.backup_path.write_text("also bad", encoding="utf-8") + + with pytest.raises(FoundationError, match="primary and backup"): + repository.load() diff --git a/tests/cognition/test_system_evaluation.py b/tests/cognition/test_system_evaluation.py new file mode 100644 index 0000000..99941ee --- /dev/null +++ b/tests/cognition/test_system_evaluation.py @@ -0,0 +1,91 @@ +"""Integrated system, persistence, ninefold, and evaluation tests.""" + +from __future__ import annotations + +import json + +import pytest + +from ix_sally.agents import AgentRole +from ix_sally.cognition import ( + CognitiveSnapshot, + CognitiveValue, + SallyCognitiveSystem, + VMStatus, + run_core_evaluation, +) +from ix_sally.foundation import FoundationError + + +def test_system_commits_vm_memory_only_after_clean_halt() -> None: + """Failed IX runs must not overwrite the system's committed runtime memory.""" + system = SallyCognitiveSystem.create() + first = system.execute_ix("remember answer = 42\n", filename="first.ix") + failed = system.execute_ix( + "remember answer = 7\nassert false\n", + filename="failed.ix", + ) + + assert first.status is VMStatus.HALTED + assert failed.status is VMStatus.FAILED + assert system.runtime_memories == {"answer": CognitiveValue.from_python(42)} + + +def test_system_state_digest_changes_after_state_transition() -> None: + """Complete state identity must change when committed cognitive state changes.""" + system = SallyCognitiveSystem.create() + before = system.digest() + system.execute_ix("remember answer = 42\n", filename="state.ix") + + assert system.digest() != before + + +def test_snapshot_round_trip_preserves_exact_payload() -> None: + """Canonical snapshot encoding must preserve all serialized state data.""" + system = SallyCognitiveSystem.create() + system.execute_ix("remember answer = 42\n", filename="snapshot.ix") + snapshot = system.snapshot() + + restored = CognitiveSnapshot.from_json(snapshot.to_json()) + + assert restored == snapshot + assert restored.state == system.state_payload() + + +def test_snapshot_rejects_tampered_state() -> None: + """Changing state bytes without updating the digest must be detected.""" + snapshot = SallyCognitiveSystem.create().snapshot() + payload = json.loads(snapshot.to_json()) + payload["state"]["execution_count"] = 999 + + with pytest.raises(FoundationError, match="digest mismatch"): + CognitiveSnapshot.from_json(json.dumps(payload)) + + +def test_ninefold_cycle_covers_each_role_once() -> None: + """Functional coordination must cover the complete canonical ninefold.""" + cycle = SallyCognitiveSystem.create().run_cycle(task="Inspect current state") + + assert len(cycle.findings) == 9 + assert {finding.role for finding in cycle.findings} == set(AgentRole) + assert cycle.digest().algorithm == "sha256" + + +def test_core_evaluation_runs_real_benchmarks_without_agi_certification() -> None: + """The built-in suite must pass its observed checks but never self-certify AGI.""" + report = run_core_evaluation() + + assert len(report.results) == 15 + assert report.passed() == 15 + assert report.overall_score() == 1.0 + assert report.agi_certified is False + assert report.classification == "experimental-cognitive-architecture" + + +def test_evaluation_is_repeatable() -> None: + """Fresh deterministic evaluations must produce identical report identities.""" + first = run_core_evaluation() + second = run_core_evaluation() + + assert first.to_payload() == second.to_payload() + assert first.digest() == second.digest() diff --git a/tests/cognition/test_values_vm.py b/tests/cognition/test_values_vm.py new file mode 100644 index 0000000..923747b --- /dev/null +++ b/tests/cognition/test_values_vm.py @@ -0,0 +1,127 @@ +"""Typed value, compiler, and VM tests.""" + +from __future__ import annotations + +import pytest + +from ix_sally.cognition import ( + BytecodeProgram, + CognitiveValue, + CognitiveValueType, + Instruction, + IXVirtualMachine, + OpCode, + VMStatus, + compile_ix_source, + value_from_payload, +) +from ix_sally.digest import DigestRecord +from ix_sally.foundation import FoundationError + + +def test_cognitive_value_preserves_exact_scalar_types() -> None: + """Boolean, integer, float, string, and null must remain distinct.""" + assert CognitiveValue.from_python(True).value_type is CognitiveValueType.BOOLEAN + assert CognitiveValue.from_python(1).value_type is CognitiveValueType.INTEGER + assert CognitiveValue.from_python(1.0).value_type is CognitiveValueType.FLOAT + assert CognitiveValue.from_python("1").value_type is CognitiveValueType.STRING + assert CognitiveValue.from_python(None).value_type is CognitiveValueType.NULL + + +def test_cognitive_value_rejects_declared_type_mismatch() -> None: + """Construction must not silently coerce a stored value.""" + with pytest.raises(FoundationError, match="does not match"): + CognitiveValue(CognitiveValueType.FLOAT, 1) + + +def test_cognitive_value_round_trip_is_exact() -> None: + """Canonical payload restoration must preserve the exact type.""" + original = CognitiveValue.from_python(4.5) + assert value_from_payload(original.to_payload()) == original + + +def test_compiler_and_vm_execute_all_statement_outputs() -> None: + """Compiled IX source must produce local, memory, output, reply, and trace state.""" + program = compile_ix_source( + "let total = 20 + 22\n" + "remember answer = total\n" + "print total\n" + "reply total\n" + "trace total == 42\n" + "assert total == 42\n", + filename="complete.ix", + ) + result = IXVirtualMachine().execute(program) + + assert result.status is VMStatus.HALTED + assert result.local_map()["total"] == CognitiveValue.from_python(42) + assert result.memory_map()["answer"] == CognitiveValue.from_python(42) + assert result.outputs == (CognitiveValue.from_python(42),) + assert result.replies == (CognitiveValue.from_python(42),) + assert result.traces == (CognitiveValue.from_python(True),) + + +def test_vm_fails_closed_on_false_assertion() -> None: + """A false assertion must return a failed receipt without raising outward.""" + result = IXVirtualMachine().execute(compile_ix_source("assert 1 == 2\n", filename="false.ix")) + + assert result.status is VMStatus.FAILED + assert result.failure is not None + assert "assertion evaluated to false" in result.failure + + +def test_vm_fails_closed_on_missing_runtime_memory() -> None: + """Recall of a statically declared but absent runtime memory must fail deterministically.""" + program = compile_ix_source( + "remember answer = 42\nrecall answer\n", + filename="memory.ix", + ) + modified = tuple( + instruction + for instruction in program.instructions + if instruction.opcode is not OpCode.STORE_MEMORY + ) + broken = BytecodeProgram.create( + instructions=modified, + source_digest=program.source_digest, + ) + + result = IXVirtualMachine().execute(broken) + + assert result.status is VMStatus.FAILED + assert result.failure is not None + assert "unknown memory name" in result.failure + + +def test_vm_reports_division_by_zero_without_partial_memory_commit() -> None: + """A runtime fault must be visible and preserve only operations completed before it.""" + result = IXVirtualMachine().execute( + compile_ix_source( + "remember safe = 1\nprint 1 / 0\nremember unsafe = 2\n", + filename="division.ix", + ) + ) + + assert result.status is VMStatus.FAILED + assert result.memory_map() == {"safe": CognitiveValue.from_python(1)} + assert "unsafe" not in result.memory_map() + + +def test_bytecode_requires_exactly_one_final_halt() -> None: + """Malformed instruction streams must be rejected before execution.""" + digest = DigestRecord.from_payload({"source": "test"}) + with pytest.raises(FoundationError, match="exactly one final halt"): + BytecodeProgram.create( + instructions=(Instruction(OpCode.HALT), Instruction(OpCode.HALT)), + source_digest=digest, + ) + + +def test_vm_result_digest_is_repeatable() -> None: + """Equivalent executions must produce the same receipt identity.""" + program = compile_ix_source("print 40 + 2\n", filename="stable.ix") + first = IXVirtualMachine().execute(program) + second = IXVirtualMachine().execute(program) + + assert first.to_payload() == second.to_payload() + assert first.digest() == second.digest() diff --git a/tests/cognition/test_world_planning.py b/tests/cognition/test_world_planning.py new file mode 100644 index 0000000..ed0f410 --- /dev/null +++ b/tests/cognition/test_world_planning.py @@ -0,0 +1,261 @@ +"""World-model, causal inference, planning, and authority tests.""" + +from __future__ import annotations + +from ix_sally.cognition import ( + ActionSpec, + CausalRule, + CognitiveValue, + DeterministicPlanner, + ExecutionPermission, + FactEffect, + FactPattern, + FactStatus, + PlanSimulator, + PlanStatus, + WorldFact, + WorldModel, +) +from ix_sally.digest import DigestRecord + + +def _observed( + fact_id: str, + subject: str, + predicate: str, + value: object, +) -> WorldFact: + assert isinstance(value, str | int | float | bool | type(None)) + return WorldFact.create( + fact_id=fact_id, + subject=subject, + predicate=predicate, + value=CognitiveValue.from_python(value), + status=FactStatus.OBSERVED, + confidence=1.0, + evidence_digests=(DigestRecord.from_payload({"fact": fact_id}),), + ) + + +def test_world_model_preserves_prediction_status() -> None: + """Rule output must remain predicted until separately observed or inferred.""" + evidence = DigestRecord.from_payload({"study": "heat"}) + model = WorldModel( + facts=(_observed("hot", "room", "temperature", "hot"),), + rules=( + CausalRule.create( + rule_id="heat-implies-cooling", + conditions=( + FactPattern.create( + subject="room", + predicate="temperature", + value=CognitiveValue.from_python("hot"), + ), + ), + effect_subject="room", + effect_predicate="needs-cooling", + effect_value=CognitiveValue.from_python(True), + confidence=0.9, + evidence_digests=(evidence,), + ), + ), + ) + + prediction = model.predict()[0] + + assert prediction.status is FactStatus.PREDICTED + assert prediction.predicate.value == "needs-cooling" + assert model.state().get(("room", "needs-cooling")) is None + + +def test_world_inference_adds_derived_fact_with_provenance() -> None: + """Inference must add an inferred fact and retain source identities.""" + source = _observed("hot", "room", "temperature", "hot") + rule = CausalRule.create( + rule_id="heat-implies-cooling", + conditions=( + FactPattern.create( + subject="room", + predicate="temperature", + value=CognitiveValue.from_python("hot"), + ), + ), + effect_subject="room", + effect_predicate="needs-cooling", + effect_value=CognitiveValue.from_python(True), + confidence=0.8, + evidence_digests=(DigestRecord.from_payload({"rule": "validated"}),), + ) + + inferred = WorldModel((source,), (rule,)).infer() + fact = inferred.state()[("room", "needs-cooling")] + + assert fact.status is FactStatus.INFERRED + assert fact.derived_from[0].value == "hot" + + +def test_counterfactual_branch_does_not_mutate_original_model() -> None: + """Hypothetical assumptions must remain isolated from observed state.""" + original = WorldModel((_observed("door", "door", "state", "closed"),), ()) + hypothetical = WorldFact.create( + fact_id="door-open-assumption", + subject="door", + predicate="state", + value=CognitiveValue.from_python("open"), + status=FactStatus.HYPOTHETICAL, + confidence=0.5, + ) + + branch = original.counterfactual((hypothetical,)) + + assert original.state()[("door", "state")].value == CognitiveValue.from_python("closed") + assert branch.state()[("door", "state")].value == CognitiveValue.from_python("open") + + +def test_planner_finds_shortest_stable_action_sequence() -> None: + """Breadth-first search must return the shortest exact-state plan.""" + model = WorldModel((_observed("start", "device", "state", "off"),), ()) + actions = ( + ActionSpec.create( + action_id="power-on", + description="Power on the device.", + preconditions=( + FactPattern.create( + subject="device", + predicate="state", + value=CognitiveValue.from_python("off"), + ), + ), + effects=( + FactEffect.create( + subject="device", + predicate="state", + value=CognitiveValue.from_python("on"), + ), + ), + cost=1.0, + risk=0.1, + ), + ActionSpec.create( + action_id="ready-device", + description="Place the powered device into ready state.", + preconditions=( + FactPattern.create( + subject="device", + predicate="state", + value=CognitiveValue.from_python("on"), + ), + ), + effects=( + FactEffect.create( + subject="device", + predicate="state", + value=CognitiveValue.from_python("ready"), + ), + ), + cost=1.0, + risk=0.1, + ), + ) + goal = FactPattern.create( + subject="device", + predicate="state", + value=CognitiveValue.from_python("ready"), + ) + + plan = DeterministicPlanner().plan( + world_model=model, + actions=actions, + goal=goal, + ) + + assert plan.status is PlanStatus.FOUND + assert tuple(action.action_id.value for action in plan.actions) == ( + "power-on", + "ready-device", + ) + + +def test_planner_reports_already_satisfied_goal() -> None: + """No-op goals must not create unnecessary action theater.""" + model = WorldModel((_observed("ready", "device", "state", "ready"),), ()) + goal = FactPattern.create( + subject="device", + predicate="state", + value=CognitiveValue.from_python("ready"), + ) + + plan = DeterministicPlanner().plan(world_model=model, actions=(), goal=goal) + + assert plan.status is PlanStatus.ALREADY_SATISFIED + assert not plan.actions + + +def test_plan_simulator_blocks_human_boundary_without_approval() -> None: + """Authority-required action must not run merely because a plan exists.""" + model = WorldModel() + action = ActionSpec.create( + action_id="external-change", + description="Simulated external change.", + preconditions=(), + effects=( + FactEffect.create( + subject="outside", + predicate="changed", + value=CognitiveValue.from_python(True), + ), + ), + cost=0.0, + risk=0.2, + authority_required=True, + ) + goal = FactPattern.create( + subject="outside", + predicate="changed", + value=CognitiveValue.from_python(True), + ) + plan = DeterministicPlanner().plan( + world_model=model, + actions=(action,), + goal=goal, + ) + + receipt = PlanSimulator().execute(plan, world_model=model) + + assert receipt.permission is ExecutionPermission.REQUIRES_HUMAN + assert receipt.resulting_model == model + + +def test_approved_plan_execution_remains_hypothetical() -> None: + """Even approved local simulation must not claim an observed outside-world change.""" + model = WorldModel() + action = ActionSpec.create( + action_id="simulate-change", + description="Simulated change.", + preconditions=(), + effects=( + FactEffect.create( + subject="simulation", + predicate="state", + value=CognitiveValue.from_python("changed"), + ), + ), + cost=0.0, + risk=0.1, + ) + goal = FactPattern.create( + subject="simulation", + predicate="state", + value=CognitiveValue.from_python("changed"), + ) + plan = DeterministicPlanner().plan( + world_model=model, + actions=(action,), + goal=goal, + ) + + receipt = PlanSimulator().execute(plan, world_model=model, human_approved=True) + + assert receipt.permission is ExecutionPermission.ALLOWED + fact = receipt.resulting_model.state()[("simulation", "state")] + assert fact.status is FactStatus.HYPOTHETICAL diff --git a/tests/language/test_ast.py b/tests/language/test_ast.py index 0d9e7bd..337b5b0 100644 --- a/tests/language/test_ast.py +++ b/tests/language/test_ast.py @@ -219,7 +219,4 @@ def test_nested_expression_walk_is_preorder() -> None: negation, name, ) - assert ( - group.to_payload()["expression"] - == negation.to_payload() - ) + assert group.to_payload()["expression"] == negation.to_payload() diff --git a/tests/language/test_errors.py b/tests/language/test_errors.py index 59f8103..d90f063 100644 --- a/tests/language/test_errors.py +++ b/tests/language/test_errors.py @@ -34,10 +34,7 @@ def test_language_diagnostic_formats_stable_message() -> None: "Unexpected closing brace. " "Hint: Remove the brace or open a matching block." ) - assert ( - diagnostic.to_payload()["formatted"] - == diagnostic.format() - ) + assert diagnostic.to_payload()["formatted"] == diagnostic.format() def test_language_diagnostic_digest_is_deterministic() -> None: diff --git a/tests/language/test_expression_parser.py b/tests/language/test_expression_parser.py index 04d9d0c..82d6e98 100644 --- a/tests/language/test_expression_parser.py +++ b/tests/language/test_expression_parser.py @@ -152,7 +152,9 @@ def test_expression_parser_rejects_trailing_primary() -> None: assert diagnostic.code.value == "syntax-unexpected-token" assert diagnostic.span.label() == "broken.ix:1:7-12" assert diagnostic.message == "Unexpected token 'ready' after expression." - def test_expression_parser_validates_supplied_token_stream() -> None: + + +def test_expression_parser_validates_supplied_token_stream() -> None: """The token-based boundary must reject malformed token sequences.""" with pytest.raises( FoundationError, diff --git a/tests/language/test_frontend.py b/tests/language/test_frontend.py index 30fc433..efb9554 100644 --- a/tests/language/test_frontend.py +++ b/tests/language/test_frontend.py @@ -46,12 +46,8 @@ def test_frontend_analyzes_complete_valid_program() -> None: def test_frontend_context_seeds_semantic_and_type_analysis() -> None: """Typed host context must remain aligned across both analysis stages.""" context = IXFrontendContext( - local_types=( - IXTypeBinding("input_value", IXValueType.FLOAT), - ), - memory_types=( - IXTypeBinding("prior_result", IXValueType.STRING), - ), + local_types=(IXTypeBinding("input_value", IXValueType.FLOAT),), + memory_types=(IXTypeBinding("prior_result", IXValueType.STRING),), ) analysis = IXFrontendAnalyzer(context=context).analyze( @@ -92,9 +88,7 @@ def test_frontend_fail_closed_boundary_raises_first_error() -> None: filename="invalid.ix", ) - assert captured.value.diagnostic.code.value == ( - "typing-invalid-operator-operands" - ) + assert captured.value.diagnostic.code.value == ("typing-invalid-operator-operands") assert captured.value.diagnostic.span.label() == "invalid.ix:1:11-21" @@ -128,7 +122,9 @@ def test_frontend_analysis_payload_and_digest_are_deterministic() -> None: assert first.to_payload()["token_count"] == len(first.tokens) assert first.to_payload()["error_count"] == 0 assert first.to_payload()["is_accepted"] is True - def test_frontend_source_digest_changes_with_source_content() -> None: + + +def test_frontend_source_digest_changes_with_source_content() -> None: """The front-end receipt must bind analysis to exact source content.""" first = analyze_ix_source("print 1", filename="source.ix") second = analyze_ix_source("print 2", filename="source.ix") diff --git a/tests/language/test_lexer.py b/tests/language/test_lexer.py index e769f7f..56d65c4 100644 --- a/tests/language/test_lexer.py +++ b/tests/language/test_lexer.py @@ -13,12 +13,7 @@ def test_lexer_tokenizes_governed_ix_source_with_exact_spans() -> None: """Keywords, values, operators, and blocks must retain source locations.""" - source = ( - "let score = 80\n" - "if score >= 75 {\n" - ' reply "pass\\nready"\n' - "}\n" - ) + source = 'let score = 80\nif score >= 75 {\n reply "pass\\nready"\n}\n' tokens = tokenize_ix(source, filename="review.ix") diff --git a/tests/language/test_statement_parser.py b/tests/language/test_statement_parser.py index 76e300b..ec4ad22 100644 --- a/tests/language/test_statement_parser.py +++ b/tests/language/test_statement_parser.py @@ -172,7 +172,9 @@ def test_statement_parser_rejects_missing_expression() -> None: diagnostic = captured.value.diagnostic assert diagnostic.code.value == "syntax-expected-expression" assert diagnostic.span.label() == "broken.ix:1:6" - def test_statement_parser_rejects_recall_trailing_tokens() -> None: + + +def test_statement_parser_rejects_recall_trailing_tokens() -> None: """Recall accepts exactly one memory name and no expression tail.""" with pytest.raises(IXSyntaxError) as captured: parse_ix_program( @@ -197,9 +199,7 @@ def test_statement_parser_rejects_unsupported_block_keyword() -> None: diagnostic = captured.value.diagnostic assert diagnostic.code.value == "syntax-unsupported-statement" assert diagnostic.span.label() == "blocked.ix:1:1-3" - assert diagnostic.message == ( - "Unsupported executable IX statement keyword 'if'." - ) + assert diagnostic.message == ("Unsupported executable IX statement keyword 'if'.") def test_statement_parser_rejects_non_keyword_statement_start() -> None: diff --git a/tests/language/test_statements.py b/tests/language/test_statements.py index 89bd6e2..6ae88f7 100644 --- a/tests/language/test_statements.py +++ b/tests/language/test_statements.py @@ -201,10 +201,13 @@ def test_program_preserves_statement_order_and_payload() -> None: second_expression, ) assert program.to_payload()["statement_count"] == 2 - assert program.digest() == Program( - span=_span(0, 17), - statements=(first, second), - ).digest() + assert ( + program.digest() + == Program( + span=_span(0, 17), + statements=(first, second), + ).digest() + ) def test_program_rejects_reordered_or_overlapping_statements() -> None: diff --git a/tests/language/test_tokens.py b/tests/language/test_tokens.py index 0d5f03c..ff9ea71 100644 --- a/tests/language/test_tokens.py +++ b/tests/language/test_tokens.py @@ -25,9 +25,7 @@ def _span(text: str = "token") -> SourceSpan: def test_keyword_vocabulary_maps_every_reserved_lexeme() -> None: """Every reserved word must have one deterministic lookup entry.""" - assert KEYWORDS_BY_LEXEME == { - keyword.value: keyword for keyword in Keyword - } + assert {keyword.value: keyword for keyword in Keyword} == KEYWORDS_BY_LEXEME assert KEYWORDS_BY_LEXEME["human_approval"] is Keyword.HUMAN_APPROVAL assert KEYWORDS_BY_LEXEME["claim_boundary"] is Keyword.CLAIM_BOUNDARY diff --git a/tests/language/test_type_system.py b/tests/language/test_type_system.py index 4e8fb0c..dd2450b 100644 --- a/tests/language/test_type_system.py +++ b/tests/language/test_type_system.py @@ -41,9 +41,7 @@ def test_type_checker_infers_local_and_memory_types() -> None: def test_type_checker_applies_numeric_promotion() -> None: """Mixed numeric arithmetic must promote deterministically to float.""" program = parse_ix_program( - "let integer_sum = 1 + 2\n" - "let mixed_sum = 1 + 2.5\n" - "let product = mixed_sum * 2\n", + "let integer_sum = 1 + 2\nlet mixed_sum = 1 + 2.5\nlet product = mixed_sum * 2\n", filename="numeric.ix", ) @@ -58,9 +56,7 @@ def test_type_checker_accepts_boolean_assertion_and_logic() -> None: """Comparisons and Boolean operators must produce assertion-safe values.""" report = check_ix_program_types( parse_ix_program( - "let score = 80\n" - "let ready = score >= 75 and true\n" - "assert ready\n", + "let score = 80\nlet ready = score >= 75 and true\nassert ready\n", filename="assertion.ix", ) ) @@ -82,18 +78,14 @@ def test_type_checker_rejects_non_boolean_assertion() -> None: diagnostic = report.errors()[0] assert diagnostic.code.value == "typing-assertion-not-boolean" assert diagnostic.span.label() == "assertion.ix:2:8-13" - assert diagnostic.message == ( - "Assert expression must be Boolean, not integer." - ) + assert diagnostic.message == ("Assert expression must be Boolean, not integer.") def test_type_checker_rejects_invalid_operator_operands() -> None: """Operators must reject statically incompatible operand categories.""" report = check_ix_program_types( parse_ix_program( - 'let bad_add = "count" + 1\n' - "let bad_logic = true and 2\n" - 'let bad_negation = -"value"\n', + 'let bad_add = "count" + 1\nlet bad_logic = true and 2\nlet bad_negation = -"value"\n', filename="operators.ix", ) ) @@ -126,12 +118,8 @@ def test_type_checker_avoids_cascading_unknown_name_errors() -> None: def test_type_context_supplies_host_types() -> None: """Host-provided local and memory types must seed static analysis.""" context = IXTypeContext( - local_types=( - IXTypeBinding("input_value", IXValueType.FLOAT), - ), - memory_types=( - IXTypeBinding("prior_result", IXValueType.STRING), - ), + local_types=(IXTypeBinding("input_value", IXValueType.FLOAT),), + memory_types=(IXTypeBinding("prior_result", IXValueType.STRING),), ) program = parse_ix_program( "let total = input_value + 1\nrecall prior_result\n", diff --git a/tests/language/test_validation.py b/tests/language/test_validation.py index c5da4ba..2f2f69d 100644 --- a/tests/language/test_validation.py +++ b/tests/language/test_validation.py @@ -18,11 +18,7 @@ def test_semantic_validator_accepts_defined_names_and_memory() -> None: """Ordered local bindings and remembered values must validate cleanly.""" program = parse_ix_program( - "let a = 10\n" - "let b = a + 5\n" - "remember total = b\n" - "recall total\n" - "assert b == 15\n", + "let a = 10\nlet b = a + 5\nremember total = b\nrecall total\nassert b == 15\n", filename="valid.ix", ) @@ -189,9 +185,7 @@ def test_require_valid_ix_program_raises_first_semantic_error() -> None: def test_empty_program_is_semantically_valid() -> None: """An empty parsed document must have a stable valid validation report.""" - report = validate_ix_program( - parse_ix_program("", filename="empty.ix") - ) + report = validate_ix_program(parse_ix_program("", filename="empty.ix")) assert report.is_valid() is True assert report.local_names == () diff --git a/tests/test_check_green.py b/tests/test_check_green.py index 8c5e950..f94745c 100644 --- a/tests/test_check_green.py +++ b/tests/test_check_green.py @@ -9,10 +9,7 @@ def test_quality_gate_commands_match_ci_tools() -> None: """The shared runner must retain every declared repository gate.""" - commands = { - gate.name: gate.command()[1:] - for gate in check_green.QUALITY_GATES - } + commands = {gate.name: gate.command()[1:] for gate in check_green.QUALITY_GATES} assert commands == { "format": ("-m", "ruff", "format", "--check", "."), @@ -37,9 +34,7 @@ def fake_run_gate( *, repository_root: Path, ) -> int: - assert repository_root == Path( - check_green.__file__ - ).resolve().parent + assert repository_root == Path(check_green.__file__).resolve().parent observed.append(gate.name) return 0 @@ -135,9 +130,7 @@ def fake_run_gate( def test_ci_workflow_delegates_each_gate_to_shared_runner() -> None: """GitHub CI must use the repository-owned gate definitions.""" repository_root = Path(check_green.__file__).resolve().parent - workflow = ( - repository_root / ".github" / "workflows" / "ci.yml" - ).read_text(encoding="utf-8") + workflow = (repository_root / ".github" / "workflows" / "ci.yml").read_text(encoding="utf-8") for gate_name in ( "format", diff --git a/tests/test_cli_cognition.py b/tests/test_cli_cognition.py new file mode 100644 index 0000000..d1d4d58 --- /dev/null +++ b/tests/test_cli_cognition.py @@ -0,0 +1,83 @@ +"""Command-line coverage for the integrated IX-Sally cognitive runtime.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from ix_sally.cli import main + + +def test_cli_cognitive_evaluation_reports_observed_results( + capsys: pytest.CaptureFixture[str], +) -> None: + """The CLI evaluation must expose actual results and preserve the AGI boundary.""" + result = main(["--cognitive-evaluation"]) + captured = capsys.readouterr() + payload = json.loads(captured.out) + + assert result == 0 + assert payload["benchmark_count"] == 15 + assert payload["passed_count"] == 15 + assert payload["agi_certified"] is False + assert payload["classification"] == "experimental-cognitive-architecture" + assert captured.err == "" + + +def test_cli_executes_ix_file_and_returns_typed_output( + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + """One UTF-8 IX program must compile and execute through the bounded VM.""" + source_path = tmp_path / "answer.ix" + source_path.write_text( + "let answer = 6 * 7\nprint answer\nassert answer == 42\n", + encoding="utf-8", + ) + + result = main(["--execute-ix", str(source_path)]) + captured = capsys.readouterr() + payload = json.loads(captured.out) + + assert result == 0 + assert payload["status"] == "halted" + assert payload["outputs"] == [{"type": "integer", "value": 42}] + assert payload["failure"] is None + assert captured.err == "" + + +def test_cli_returns_failure_for_failed_ix_assertion( + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + """A failed IX assertion must produce a nonzero CLI result and a receipt.""" + source_path = tmp_path / "failure.ix" + source_path.write_text("assert false\n", encoding="utf-8") + + result = main(["--execute-ix", str(source_path)]) + captured = capsys.readouterr() + payload = json.loads(captured.out) + + assert result == 1 + assert payload["status"] == "failed" + assert payload["failure"] is not None + assert captured.err == "" + + +def test_cli_empty_snapshot_is_complete_and_named_ix_sally( + capsys: pytest.CaptureFixture[str], +) -> None: + """The clean snapshot must contain the complete integrated repository state.""" + result = main(["--empty-cognitive-snapshot"]) + captured = capsys.readouterr() + payload = json.loads(captured.out) + + assert result == 0 + assert payload["repository"] == "IX-Sally" + assert payload["schema_version"] == 1 + assert payload["state"]["repository"] == "IX-Sally" + assert payload["state"]["goals"] == {"goals": []} + assert payload["state"]["episodes"] == {"episodes": []} + assert captured.err == "" diff --git a/tests/test_dependency_check.py b/tests/test_dependency_check.py index 93abaaa..b2d512e 100644 --- a/tests/test_dependency_check.py +++ b/tests/test_dependency_check.py @@ -21,9 +21,7 @@ def _create_package(path: Path) -> None: def test_repository_runtime_dependency_graph_is_acyclic() -> None: """The current IX-Sally runtime graph must remain cycle-free.""" - repository_root = Path( - dependency_check.__file__ - ).resolve().parent + repository_root = Path(dependency_check.__file__).resolve().parent graph = dependency_check.build_dependency_graph( source_root=repository_root / "src", @@ -81,9 +79,7 @@ def test_dependency_graph_ignores_type_checking_imports( encoding="utf-8", ) (package_root / "alpha.py").write_text( - "from typing import TYPE_CHECKING\n" - "if TYPE_CHECKING:\n" - " from ix_sally.beta import Beta\n", + "from typing import TYPE_CHECKING\nif TYPE_CHECKING:\n from ix_sally.beta import Beta\n", encoding="utf-8", ) (package_root / "beta.py").write_text( @@ -112,9 +108,7 @@ def test_dependency_graph_ignores_qualified_type_checking_imports( package_root = tmp_path / "ix_sally" _create_package(package_root) (package_root / "alpha.py").write_text( - "import typing\n" - "if typing.TYPE_CHECKING:\n" - " from . import beta\n", + "import typing\nif typing.TYPE_CHECKING:\n from . import beta\n", encoding="utf-8", ) (package_root / "beta.py").write_text( @@ -126,11 +120,14 @@ def test_dependency_graph_ignores_qualified_type_checking_imports( source_root=tmp_path, ) - assert dependency_check.ModuleDependency( - source="ix_sally.alpha", - target="ix_sally.beta", - line=3, - ) not in graph.dependencies + assert ( + dependency_check.ModuleDependency( + source="ix_sally.alpha", + target="ix_sally.beta", + line=3, + ) + not in graph.dependencies + ) def test_dependency_graph_resolves_relative_submodule_imports( @@ -154,16 +151,22 @@ def test_dependency_graph_resolves_relative_submodule_imports( source_root=tmp_path, ) - assert dependency_check.ModuleDependency( - source="ix_sally.language.alpha", - target="ix_sally.language.beta", - line=1, - ) in graph.dependencies - assert dependency_check.ModuleDependency( - source="ix_sally.language.beta", - target="ix_sally.language.alpha", - line=1, - ) in graph.dependencies + assert ( + dependency_check.ModuleDependency( + source="ix_sally.language.alpha", + target="ix_sally.language.beta", + line=1, + ) + in graph.dependencies + ) + assert ( + dependency_check.ModuleDependency( + source="ix_sally.language.beta", + target="ix_sally.language.alpha", + line=1, + ) + in graph.dependencies + ) assert graph.cycles() == ( ( "ix_sally.language.alpha", @@ -181,8 +184,7 @@ def test_dependency_graph_resolves_parent_relative_imports( _create_package(package_root) _create_package(language_root) (package_root / "foundation.py").write_text( - "class FoundationError(Exception):\n" - " pass\n", + "class FoundationError(Exception):\n pass\n", encoding="utf-8", ) (language_root / "runtime.py").write_text( @@ -194,11 +196,14 @@ def test_dependency_graph_resolves_parent_relative_imports( source_root=tmp_path, ) - assert dependency_check.ModuleDependency( - source="ix_sally.language.runtime", - target="ix_sally.foundation", - line=1, - ) in graph.dependencies + assert ( + dependency_check.ModuleDependency( + source="ix_sally.language.runtime", + target="ix_sally.foundation", + line=1, + ) + in graph.dependencies + ) def test_dependency_graph_resolves_package_member_modules( @@ -220,11 +225,14 @@ def test_dependency_graph_resolves_package_member_modules( source_root=tmp_path, ) - assert dependency_check.ModuleDependency( - source="ix_sally.bridge", - target="ix_sally.state", - line=1, - ) in graph.dependencies + assert ( + dependency_check.ModuleDependency( + source="ix_sally.bridge", + target="ix_sally.state", + line=1, + ) + in graph.dependencies + ) def test_dependency_check_main_reports_repository_summary( diff --git a/tests/test_human_review_audited_reentry_ledger.py b/tests/test_human_review_audited_reentry_ledger.py index 52795c7..788bfe2 100644 --- a/tests/test_human_review_audited_reentry_ledger.py +++ b/tests/test_human_review_audited_reentry_ledger.py @@ -133,12 +133,8 @@ def test_audited_reentry_ledger_appends_result_at_next_sequence() -> None: assert updated.failed_entries() == () assert updated.changed_state_entries() == (latest,) assert updated.entries_for_stage(RunStage.FORGE_DISPATCH) == (latest,) - assert updated.entries_for_audit_status(HumanReviewReentryAuditStatus.PASSED) == ( - latest, - ) - assert updated.entries_for_reentry_status(HumanReviewReentryStatus.ADVANCED) == ( - latest, - ) + assert updated.entries_for_audit_status(HumanReviewReentryAuditStatus.PASSED) == (latest,) + assert updated.entries_for_reentry_status(HumanReviewReentryStatus.ADVANCED) == (latest,) def test_audited_reentry_ledger_tracks_waiting_external_input_result() -> None: @@ -206,9 +202,7 @@ def test_audited_reentry_ledger_entry_rejects_invalid_step_counts() -> None: resume_operation_digest=result.receipt.resume_operation_digest, reentry_coordination_digest=result.receipt.reentry_coordination_digest, audit_report_digest=result.receipt.audit_report_digest, - audit_workflow_operation_digest=( - result.receipt.audit_workflow_operation_digest - ), + audit_workflow_operation_digest=(result.receipt.audit_workflow_operation_digest), before_state_digest=result.receipt.before_state_digest, after_state_digest=result.receipt.after_state_digest, before_control_plane_digest=result.receipt.before_control_plane_digest, @@ -230,9 +224,7 @@ def test_audited_reentry_ledger_entry_rejects_invalid_step_counts() -> None: resume_operation_digest=result.receipt.resume_operation_digest, reentry_coordination_digest=result.receipt.reentry_coordination_digest, audit_report_digest=result.receipt.audit_report_digest, - audit_workflow_operation_digest=( - result.receipt.audit_workflow_operation_digest - ), + audit_workflow_operation_digest=(result.receipt.audit_workflow_operation_digest), before_state_digest=result.receipt.before_state_digest, after_state_digest=result.receipt.after_state_digest, before_control_plane_digest=result.receipt.before_control_plane_digest, diff --git a/tests/test_human_review_bundle.py b/tests/test_human_review_bundle.py index 0af9a44..8edc446 100644 --- a/tests/test_human_review_bundle.py +++ b/tests/test_human_review_bundle.py @@ -90,8 +90,12 @@ def test_human_review_bundle_assembler_accepts_custom_authority_note() -> None: def test_human_review_bundle_assembler_combines_gateway_and_manual_cards() -> None: - state = _state().with_action(_review_action()).with_evidence_support_finding( - _unsupported_finding(), + state = ( + _state() + .with_action(_review_action()) + .with_evidence_support_finding( + _unsupported_finding(), + ) ) bundle = HumanReviewBundleAssembler.create().assemble(state=state) diff --git a/tests/test_human_review_bundle_ledger.py b/tests/test_human_review_bundle_ledger.py index 8fead70..9786a65 100644 --- a/tests/test_human_review_bundle_ledger.py +++ b/tests/test_human_review_bundle_ledger.py @@ -141,8 +141,12 @@ def test_human_review_bundle_ledger_rejects_mismatched_counts() -> None: def test_human_review_bundle_ledger_filters_manual_investigation_entries() -> None: - state = _state().with_action(_review_action()).with_evidence_support_finding( - _unsupported_finding(), + state = ( + _state() + .with_action(_review_action()) + .with_evidence_support_finding( + _unsupported_finding(), + ) ) bundle = HumanReviewBundleAssembler.create().assemble(state=state) ledger = HumanReviewBundleLedger.create(()).append_bundle(bundle) diff --git a/tests/test_human_review_clearance.py b/tests/test_human_review_clearance.py index 5c16a91..89ae348 100644 --- a/tests/test_human_review_clearance.py +++ b/tests/test_human_review_clearance.py @@ -139,9 +139,7 @@ def test_clearance_report_marks_approved_gateway_decision_as_cleared() -> None: def test_clearance_report_marks_deferred_gateway_decision_open() -> None: assessment = _assessment_for_decision(HumanReviewDecisionStatus.DEFERRED) - assert assessment.clearance_report.status is ( - HumanReviewClearanceStatus.DEFERRED_DECISION_OPEN - ) + assert assessment.clearance_report.status is (HumanReviewClearanceStatus.DEFERRED_DECISION_OPEN) assert assessment.cleared_to_resume() is False assert assessment.clearance_report.deferred_decision_count == 1 assert assessment.clearance_report.has_blocking_decision() is True diff --git a/tests/test_human_review_complete_reentry.py b/tests/test_human_review_complete_reentry.py index 237a270..10ddd54 100644 --- a/tests/test_human_review_complete_reentry.py +++ b/tests/test_human_review_complete_reentry.py @@ -97,9 +97,7 @@ def test_complete_reentry_coordinator_records_final_audited_result() -> None: ) assert result.reentry_status() is HumanReviewReentryStatus.ADVANCED assert result.audit_status() is HumanReviewReentryAuditStatus.PASSED - assert result.report_status() is ( - HumanReviewControlPlaneReportStatus.AUDITED_REENTRY_ACCEPTED - ) + assert result.report_status() is (HumanReviewControlPlaneReportStatus.AUDITED_REENTRY_ACCEPTED) assert result.final_stage() is RunStage.FORGE_DISPATCH assert result.changed_state() is True assert result.accepted() is True diff --git a/tests/test_human_review_complete_reentry_closeout_coordination.py b/tests/test_human_review_complete_reentry_closeout_coordination.py index 877de4c..c3adc00 100644 --- a/tests/test_human_review_complete_reentry_closeout_coordination.py +++ b/tests/test_human_review_complete_reentry_closeout_coordination.py @@ -84,12 +84,9 @@ def _resume_operation(): def test_closeout_coordinator_records_accepted_complete_reentry_closeout() -> None: - result = ( - CompleteHumanReviewReentryCloseoutCoordinator.create() - .resume_closeout_and_record( - resume_operation=_resume_operation(), - max_steps=1, - ) + result = CompleteHumanReviewReentryCloseoutCoordinator.create().resume_closeout_and_record( + resume_operation=_resume_operation(), + max_steps=1, ) assert result.accepted() is True @@ -111,24 +108,17 @@ def test_closeout_coordinator_records_accepted_complete_reentry_closeout() -> No def test_closeout_coordinator_records_waiting_complete_reentry_closeout() -> None: - result = ( - CompleteHumanReviewReentryCloseoutCoordinator.create() - .resume_closeout_and_record( - resume_operation=_resume_operation(), - max_steps=3, - ) + result = CompleteHumanReviewReentryCloseoutCoordinator.create().resume_closeout_and_record( + resume_operation=_resume_operation(), + max_steps=3, ) assert result.accepted() is False assert result.waiting_for_external_input() is True assert result.blocked() is False assert result.final_stage() is RunStage.FORGE_RESULT_PROCESSING - assert result.receipt.reentry_status is ( - HumanReviewReentryStatus.WAITING_FOR_EXTERNAL_INPUT - ) - assert result.receipt.audit_status is ( - HumanReviewReentryAuditStatus.WAITING_FOR_EXTERNAL_INPUT - ) + assert result.receipt.reentry_status is (HumanReviewReentryStatus.WAITING_FOR_EXTERNAL_INPUT) + assert result.receipt.audit_status is (HumanReviewReentryAuditStatus.WAITING_FOR_EXTERNAL_INPUT) assert result.closeout_report.closeout_status is ( CompleteHumanReviewReentryCloseoutStatus.WAITING_FOR_EXTERNAL_INPUT ) @@ -138,19 +128,14 @@ def test_closeout_coordinator_records_waiting_complete_reentry_closeout() -> Non def test_closeout_coordination_receipt_links_layers() -> None: - result = ( - CompleteHumanReviewReentryCloseoutCoordinator.create() - .resume_closeout_and_record( - resume_operation=_resume_operation(), - max_steps=1, - ) + result = CompleteHumanReviewReentryCloseoutCoordinator.create().resume_closeout_and_record( + resume_operation=_resume_operation(), + max_steps=1, ) receipt = result.receipt - assert receipt.complete_reentry_result_digest == ( - result.complete_reentry_result.digest() - ) + assert receipt.complete_reentry_result_digest == (result.complete_reentry_result.digest()) assert receipt.complete_reentry_receipt_digest == ( result.complete_reentry_result.receipt.digest() ) @@ -161,18 +146,13 @@ def test_closeout_coordination_receipt_links_layers() -> None: assert receipt.recorded_complete_reentry() is True assert receipt.recorded_closeout() is True assert receipt.changed_state() is True - assert receipt.report_status is ( - HumanReviewControlPlaneReportStatus.COMPLETE_REENTRY_ACCEPTED - ) + assert receipt.report_status is (HumanReviewControlPlaneReportStatus.COMPLETE_REENTRY_ACCEPTED) def test_closeout_coordination_payload_and_digest_are_stable() -> None: - result = ( - CompleteHumanReviewReentryCloseoutCoordinator.create() - .resume_closeout_and_record( - resume_operation=_resume_operation(), - max_steps=1, - ) + result = CompleteHumanReviewReentryCloseoutCoordinator.create().resume_closeout_and_record( + resume_operation=_resume_operation(), + max_steps=1, ) payload = result.to_payload() diff --git a/tests/test_human_review_complete_reentry_closeout_coordination_ledger.py b/tests/test_human_review_complete_reentry_closeout_coordination_ledger.py index 65796e4..6d545d9 100644 --- a/tests/test_human_review_complete_reentry_closeout_coordination_ledger.py +++ b/tests/test_human_review_complete_reentry_closeout_coordination_ledger.py @@ -116,10 +116,7 @@ def test_closeout_coordination_ledger_entry_records_result() -> None: def test_closeout_coordination_ledger_appends_result() -> None: result = _coordination_result(max_steps=1) - updated = ( - CompleteHumanReviewReentryCloseoutCoordinationLedger.create(()) - .append_result(result) - ) + updated = CompleteHumanReviewReentryCloseoutCoordinationLedger.create(()).append_result(result) latest = updated.latest() assert latest is not None @@ -136,10 +133,7 @@ def test_closeout_coordination_ledger_appends_result() -> None: def test_closeout_coordination_ledger_tracks_waiting_result() -> None: result = _coordination_result(max_steps=3) - updated = ( - CompleteHumanReviewReentryCloseoutCoordinationLedger.create(()) - .append_result(result) - ) + updated = CompleteHumanReviewReentryCloseoutCoordinationLedger.create(()).append_result(result) latest = updated.latest() assert latest is not None @@ -167,9 +161,7 @@ def test_closeout_coordination_ledger_rejects_duplicate_result_digest() -> None: FoundationError, match="duplicate complete reentry closeout coordination result digest", ): - CompleteHumanReviewReentryCloseoutCoordinationLedger.create( - (first, duplicate) - ) + CompleteHumanReviewReentryCloseoutCoordinationLedger.create((first, duplicate)) def test_closeout_coordination_ledger_rejects_duplicate_sequence() -> None: @@ -198,16 +190,10 @@ def test_closeout_coordination_ledger_entry_rejects_invalid_steps() -> None: coordination_result_digest=result.digest(), coordination_receipt_digest=result.receipt.digest(), resume_operation_digest=result.receipt.resume_operation_digest, - complete_reentry_result_digest=( - result.receipt.complete_reentry_result_digest - ), - complete_reentry_receipt_digest=( - result.receipt.complete_reentry_receipt_digest - ), + complete_reentry_result_digest=(result.receipt.complete_reentry_result_digest), + complete_reentry_receipt_digest=(result.receipt.complete_reentry_receipt_digest), closeout_report_digest=result.receipt.closeout_report_digest, - closeout_workflow_operation_digest=( - result.receipt.closeout_workflow_operation_digest - ), + closeout_workflow_operation_digest=(result.receipt.closeout_workflow_operation_digest), before_state_digest=result.receipt.before_state_digest, after_state_digest=result.receipt.after_state_digest, before_control_plane_digest=result.receipt.before_control_plane_digest, @@ -226,14 +212,8 @@ def test_closeout_coordination_ledger_entry_rejects_invalid_steps() -> None: def test_closeout_coordination_ledger_payload_and_digest_are_stable() -> None: result = _coordination_result(max_steps=1) - first = ( - CompleteHumanReviewReentryCloseoutCoordinationLedger.create(()) - .append_result(result) - ) - second = ( - CompleteHumanReviewReentryCloseoutCoordinationLedger.create(()) - .append_result(result) - ) + first = CompleteHumanReviewReentryCloseoutCoordinationLedger.create(()).append_result(result) + second = CompleteHumanReviewReentryCloseoutCoordinationLedger.create(()).append_result(result) payload = first.to_payload() latest = first.latest() diff --git a/tests/test_human_review_complete_reentry_ledger.py b/tests/test_human_review_complete_reentry_ledger.py index d1f26de..5171f63 100644 --- a/tests/test_human_review_complete_reentry_ledger.py +++ b/tests/test_human_review_complete_reentry_ledger.py @@ -108,9 +108,7 @@ def test_complete_reentry_ledger_entry_records_result() -> None: assert entry.final_stage is RunStage.FORGE_DISPATCH assert entry.reentry_status is HumanReviewReentryStatus.ADVANCED assert entry.audit_status is HumanReviewReentryAuditStatus.PASSED - assert entry.report_status is ( - HumanReviewControlPlaneReportStatus.AUDITED_REENTRY_ACCEPTED - ) + assert entry.report_status is (HumanReviewControlPlaneReportStatus.AUDITED_REENTRY_ACCEPTED) assert entry.changed_state() is True assert entry.recorded_reentry_and_audit() is True assert entry.recorded_complete_audited_reentry() is True @@ -193,9 +191,7 @@ def test_complete_reentry_ledger_entry_rejects_invalid_step_counts() -> None: resume_operation_digest=result.receipt.resume_operation_digest, audited_reentry_result_digest=result.receipt.audited_reentry_result_digest, audited_reentry_receipt_digest=result.receipt.audited_reentry_receipt_digest, - final_workflow_operation_digest=( - result.receipt.final_workflow_operation_digest - ), + final_workflow_operation_digest=(result.receipt.final_workflow_operation_digest), before_state_digest=result.receipt.before_state_digest, after_state_digest=result.receipt.after_state_digest, before_control_plane_digest=result.receipt.before_control_plane_digest, diff --git a/tests/test_human_review_complete_reentry_report_ledger.py b/tests/test_human_review_complete_reentry_report_ledger.py index 02882fa..31683a1 100644 --- a/tests/test_human_review_complete_reentry_report_ledger.py +++ b/tests/test_human_review_complete_reentry_report_ledger.py @@ -106,9 +106,7 @@ def test_complete_reentry_closeout_ledger_entry_records_report() -> None: assert entry.sequence == 1 assert entry.closeout_report_digest == report.digest() - assert entry.complete_reentry_result_digest == ( - report.complete_reentry_result_digest - ) + assert entry.complete_reentry_result_digest == (report.complete_reentry_result_digest) assert entry.closeout_status is CompleteHumanReviewReentryCloseoutStatus.ACCEPTED assert entry.final_stage is RunStage.FORGE_DISPATCH assert entry.finding_count == 6 diff --git a/tests/test_human_review_control_plane.py b/tests/test_human_review_control_plane.py index 32572ca..0cc0f32 100644 --- a/tests/test_human_review_control_plane.py +++ b/tests/test_human_review_control_plane.py @@ -22,7 +22,6 @@ from ix_sally.human_review_resume import HumanReviewResumeCoordinator from ix_sally.human_review_resume_ledger import HumanReviewResumeLedger from ix_sally.runtime import NinefoldRuntimeKit -from ix_sally.stage_readiness import RunStage from ix_sally.state import NinefoldRunState @@ -68,7 +67,10 @@ def _control_state_after_handoff() -> HumanReviewControlPlaneState: return HumanReviewControlPlaneState.create().with_handoff_ledger(handoff_ledger) -def _control_state_after_approved_decision() -> tuple[HumanReviewControlPlaneState, NinefoldRunState]: +def _control_state_after_approved_decision() -> tuple[ + HumanReviewControlPlaneState, + NinefoldRunState, +]: state = _state().with_action(_review_action()) bundle = HumanReviewBundleAssembler.create().assemble(state=state) handoff_ledger = HumanReviewBundleLedger.create(()).append_bundle(bundle) diff --git a/tests/test_human_review_control_plane_audited_reentry.py b/tests/test_human_review_control_plane_audited_reentry.py index b221eed..178f5e0 100644 --- a/tests/test_human_review_control_plane_audited_reentry.py +++ b/tests/test_human_review_control_plane_audited_reentry.py @@ -68,9 +68,7 @@ def test_control_plane_state_tracks_audited_reentry_ledger() -> None: assert state.has_recorded_audited_reentries() is True assert state.latest_audited_reentry_digest() == ledger.latest().digest().value assert state.to_payload()["audited_reentry_count"] == 1 - assert state.to_payload()["latest_audited_reentry_digest"] == ( - ledger.latest().digest().value - ) + assert state.to_payload()["latest_audited_reentry_digest"] == (ledger.latest().digest().value) def test_control_plane_status_tracks_audited_reentry_counts() -> None: @@ -101,9 +99,7 @@ def test_control_plane_snapshot_includes_audited_reentry_ledger_digest() -> None snapshot = HumanReviewControlPlaneSnapshot.from_state(state) payload = snapshot.to_payload() - assert payload["audited_reentry_ledger_digest"] == ( - state.audited_reentry_ledger.digest().value - ) + assert payload["audited_reentry_ledger_digest"] == (state.audited_reentry_ledger.digest().value) assert payload["status"]["audited_reentry_count"] == 1 @@ -137,9 +133,7 @@ def test_control_plane_snapshot_rejects_audited_reentry_count_mismatch() -> None operator_attention_audited_reentry_count=( good_status.operator_attention_audited_reentry_count ), - changed_state_audited_reentry_count=( - good_status.changed_state_audited_reentry_count - ), + changed_state_audited_reentry_count=(good_status.changed_state_audited_reentry_count), ) with pytest.raises(FoundationError, match="audited reentry count mismatch"): diff --git a/tests/test_human_review_control_plane_complete_reentry.py b/tests/test_human_review_control_plane_complete_reentry.py index 60eb23e..8978b7b 100644 --- a/tests/test_human_review_control_plane_complete_reentry.py +++ b/tests/test_human_review_control_plane_complete_reentry.py @@ -68,9 +68,7 @@ def test_control_plane_state_tracks_complete_reentry_ledger() -> None: assert state.has_recorded_complete_reentries() is True assert state.latest_complete_reentry_digest() == ledger.latest().digest().value assert state.to_payload()["complete_reentry_count"] == 1 - assert state.to_payload()["latest_complete_reentry_digest"] == ( - ledger.latest().digest().value - ) + assert state.to_payload()["latest_complete_reentry_digest"] == (ledger.latest().digest().value) def test_control_plane_status_tracks_complete_reentry_counts() -> None: @@ -137,9 +135,7 @@ def test_control_plane_snapshot_rejects_complete_reentry_count_mismatch() -> Non operator_attention_audited_reentry_count=( good_status.operator_attention_audited_reentry_count ), - changed_state_audited_reentry_count=( - good_status.changed_state_audited_reentry_count - ), + changed_state_audited_reentry_count=(good_status.changed_state_audited_reentry_count), complete_reentry_count=0, accepted_complete_reentry_count=good_status.accepted_complete_reentry_count, failed_complete_reentry_count=good_status.failed_complete_reentry_count, @@ -147,9 +143,7 @@ def test_control_plane_snapshot_rejects_complete_reentry_count_mismatch() -> Non operator_attention_complete_reentry_count=( good_status.operator_attention_complete_reentry_count ), - changed_state_complete_reentry_count=( - good_status.changed_state_complete_reentry_count - ), + changed_state_complete_reentry_count=(good_status.changed_state_complete_reentry_count), ) with pytest.raises(FoundationError, match="complete reentry count mismatch"): diff --git a/tests/test_human_review_control_plane_complete_reentry_closeout.py b/tests/test_human_review_control_plane_complete_reentry_closeout.py index 631d4f3..94ad5fd 100644 --- a/tests/test_human_review_control_plane_complete_reentry_closeout.py +++ b/tests/test_human_review_control_plane_complete_reentry_closeout.py @@ -67,15 +67,11 @@ def test_control_plane_state_defaults_to_empty_closeout_ledger() -> None: def test_control_plane_state_tracks_closeout_ledger() -> None: ledger = _closeout_ledger() - state = HumanReviewControlPlaneState.create().with_complete_reentry_closeout_ledger( - ledger - ) + state = HumanReviewControlPlaneState.create().with_complete_reentry_closeout_ledger(ledger) assert state.complete_reentry_closeout_count() == 1 assert state.has_recorded_complete_reentry_closeouts() is True - assert state.latest_complete_reentry_closeout_digest() == ( - ledger.latest().digest().value - ) + assert state.latest_complete_reentry_closeout_digest() == (ledger.latest().digest().value) assert state.to_payload()["complete_reentry_closeout_count"] == 1 assert state.to_payload()["latest_complete_reentry_closeout_digest"] == ( ledger.latest().digest().value @@ -145,9 +141,7 @@ def test_control_plane_snapshot_rejects_closeout_count_mismatch() -> None: operator_attention_audited_reentry_count=( good_status.operator_attention_audited_reentry_count ), - changed_state_audited_reentry_count=( - good_status.changed_state_audited_reentry_count - ), + changed_state_audited_reentry_count=(good_status.changed_state_audited_reentry_count), complete_reentry_count=good_status.complete_reentry_count, accepted_complete_reentry_count=good_status.accepted_complete_reentry_count, failed_complete_reentry_count=good_status.failed_complete_reentry_count, @@ -155,9 +149,7 @@ def test_control_plane_snapshot_rejects_closeout_count_mismatch() -> None: operator_attention_complete_reentry_count=( good_status.operator_attention_complete_reentry_count ), - changed_state_complete_reentry_count=( - good_status.changed_state_complete_reentry_count - ), + changed_state_complete_reentry_count=(good_status.changed_state_complete_reentry_count), complete_reentry_closeout_count=0, accepted_complete_reentry_closeout_count=( good_status.accepted_complete_reentry_closeout_count diff --git a/tests/test_human_review_control_plane_complete_reentry_closeout_operation.py b/tests/test_human_review_control_plane_complete_reentry_closeout_operation.py index 66c370a..dc4b9e5 100644 --- a/tests/test_human_review_control_plane_complete_reentry_closeout_operation.py +++ b/tests/test_human_review_control_plane_complete_reentry_closeout_operation.py @@ -85,12 +85,9 @@ def _resume_operation(): def _complete_reentry(max_steps: int = 1) -> CompleteHumanReviewReentryResult: - return ( - CompleteHumanReviewReentryCoordinator.create() - .resume_audit_record_and_finalize( - resume_operation=_resume_operation(), - max_steps=max_steps, - ) + return CompleteHumanReviewReentryCoordinator.create().resume_audit_record_and_finalize( + resume_operation=_resume_operation(), + max_steps=max_steps, ) @@ -104,12 +101,9 @@ def test_control_plane_coordinator_records_complete_reentry_closeout_operation() complete_reentry = _complete_reentry() closeout_report = _closeout_report(complete_reentry) - result = ( - HumanReviewControlPlaneCoordinator.create() - .record_complete_reentry_closeout( - closeout_report=closeout_report, - control_plane=complete_reentry.control_plane, - ) + result = HumanReviewControlPlaneCoordinator.create().record_complete_reentry_closeout( + closeout_report=closeout_report, + control_plane=complete_reentry.control_plane, ) assert result.operation_kind() is ( @@ -119,13 +113,9 @@ def test_control_plane_coordinator_records_complete_reentry_closeout_operation() assert result.before_control_plane.complete_reentry_closeout_count() == 0 assert result.after_control_plane.complete_reentry_closeout_count() == 1 assert result.after_control_plane.latest_complete_reentry_closeout_digest() == ( - result.after_control_plane.complete_reentry_closeout_ledger.latest() - .digest() - .value - ) - assert result.require_complete_reentry_closeout_report().digest() == ( - closeout_report.digest() + result.after_control_plane.complete_reentry_closeout_ledger.latest().digest().value ) + assert result.require_complete_reentry_closeout_report().digest() == (closeout_report.digest()) assert result.receipt.complete_reentry_closeout_count == 1 @@ -133,12 +123,9 @@ def test_control_plane_complete_reentry_closeout_payload_links_report() -> None: complete_reentry = _complete_reentry() closeout_report = _closeout_report(complete_reentry) - result = ( - HumanReviewControlPlaneCoordinator.create() - .record_complete_reentry_closeout( - closeout_report=closeout_report, - control_plane=complete_reentry.control_plane, - ) + result = HumanReviewControlPlaneCoordinator.create().record_complete_reentry_closeout( + closeout_report=closeout_report, + control_plane=complete_reentry.control_plane, ) payload = result.to_payload() @@ -146,9 +133,7 @@ def test_control_plane_complete_reentry_closeout_payload_links_report() -> None: HumanReviewControlPlaneOperationKind.COMPLETE_REENTRY_CLOSEOUT_RECORDED.value ) assert payload["complete_reentry_closeout_count"] == 1 - assert payload["complete_reentry_closeout_report_digest"] == ( - closeout_report.digest().value - ) + assert payload["complete_reentry_closeout_report_digest"] == (closeout_report.digest().value) assert payload["complete_reentry_count"] == 1 assert payload["audited_reentry_count"] == 1 diff --git a/tests/test_human_review_control_plane_imports.py b/tests/test_human_review_control_plane_imports.py index d7a80b5..ba10cd2 100644 --- a/tests/test_human_review_control_plane_imports.py +++ b/tests/test_human_review_control_plane_imports.py @@ -10,7 +10,7 @@ @pytest.mark.parametrize( "modules", - ( + [ ( "ix_sally.human_review_control_plane", "ix_sally.human_review_control_plane_report", @@ -19,7 +19,7 @@ "ix_sally.human_review_control_plane_report", "ix_sally.human_review_control_plane", ), - ), + ], ) def test_control_plane_modules_import_in_either_order( modules: tuple[str, str], diff --git a/tests/test_human_review_control_plane_reentry_audit.py b/tests/test_human_review_control_plane_reentry_audit.py index 891db95..cf1c00e 100644 --- a/tests/test_human_review_control_plane_reentry_audit.py +++ b/tests/test_human_review_control_plane_reentry_audit.py @@ -62,15 +62,11 @@ def test_control_plane_state_tracks_reentry_audit_ledger() -> None: assert state.has_recorded_reentry_audits() is True assert state.latest_reentry_audit_digest() == ledger.latest().digest().value assert state.to_payload()["reentry_audit_count"] == 1 - assert state.to_payload()["latest_reentry_audit_digest"] == ( - ledger.latest().digest().value - ) + assert state.to_payload()["latest_reentry_audit_digest"] == (ledger.latest().digest().value) def test_control_plane_status_tracks_reentry_audit_counts() -> None: - state = HumanReviewControlPlaneState.create().with_reentry_audit_ledger( - _reentry_audit_ledger() - ) + state = HumanReviewControlPlaneState.create().with_reentry_audit_ledger(_reentry_audit_ledger()) status = HumanReviewControlPlaneStatus.from_state(state) @@ -87,23 +83,17 @@ def test_control_plane_status_tracks_reentry_audit_counts() -> None: def test_control_plane_snapshot_includes_reentry_audit_ledger_digest() -> None: - state = HumanReviewControlPlaneState.create().with_reentry_audit_ledger( - _reentry_audit_ledger() - ) + state = HumanReviewControlPlaneState.create().with_reentry_audit_ledger(_reentry_audit_ledger()) snapshot = HumanReviewControlPlaneSnapshot.from_state(state) payload = snapshot.to_payload() - assert payload["reentry_audit_ledger_digest"] == ( - state.reentry_audit_ledger.digest().value - ) + assert payload["reentry_audit_ledger_digest"] == (state.reentry_audit_ledger.digest().value) assert payload["status"]["reentry_audit_count"] == 1 def test_control_plane_snapshot_rejects_reentry_audit_count_mismatch() -> None: - state = HumanReviewControlPlaneState.create().with_reentry_audit_ledger( - _reentry_audit_ledger() - ) + state = HumanReviewControlPlaneState.create().with_reentry_audit_ledger(_reentry_audit_ledger()) good_status = HumanReviewControlPlaneStatus.from_state(state) bad_status = HumanReviewControlPlaneStatus( state_digest=state.digest(), diff --git a/tests/test_human_review_control_plane_reentry_audit_operation.py b/tests/test_human_review_control_plane_reentry_audit_operation.py index 36b0633..b605df7 100644 --- a/tests/test_human_review_control_plane_reentry_audit_operation.py +++ b/tests/test_human_review_control_plane_reentry_audit_operation.py @@ -95,9 +95,7 @@ def test_control_plane_coordinator_records_reentry_audit_operation() -> None: control_plane=audit_report_to_control_plane(audit_report), ) - assert result.operation_kind() is ( - HumanReviewControlPlaneOperationKind.REENTRY_AUDIT_RECORDED - ) + assert result.operation_kind() is (HumanReviewControlPlaneOperationKind.REENTRY_AUDIT_RECORDED) assert result.changed_control_plane() is True assert result.before_control_plane.reentry_audit_count() == 0 assert result.after_control_plane.reentry_audit_count() == 1 diff --git a/tests/test_human_review_control_plane_reentry_operation.py b/tests/test_human_review_control_plane_reentry_operation.py index 18f1abd..6887dc0 100644 --- a/tests/test_human_review_control_plane_reentry_operation.py +++ b/tests/test_human_review_control_plane_reentry_operation.py @@ -32,7 +32,9 @@ def _state() -> NinefoldRunState: return NinefoldRunState.create(runtime_kit=NinefoldRuntimeKit.create(contract=contract)) -def _review_action(description: str = "Run certified post-review verification.") -> BoundedActionRecord: +def _review_action( + description: str = "Run certified post-review verification.", +) -> BoundedActionRecord: action = BoundedActionRecord.create( cycle=1, proposed_by=AgentRole.SALLY, diff --git a/tests/test_human_review_docket.py b/tests/test_human_review_docket.py index c73d388..c1d6619 100644 --- a/tests/test_human_review_docket.py +++ b/tests/test_human_review_docket.py @@ -18,7 +18,6 @@ HumanReviewDocketTargetType, ) from ix_sally.runtime import NinefoldRuntimeKit -from ix_sally.stage_readiness import RunStage from ix_sally.state import NinefoldRunState @@ -108,9 +107,7 @@ def test_human_review_docket_builder_collects_evidence_support_target() -> None: docket = HumanReviewDocketBuilder.create().build(state=state) assert docket.to_payload()["evidence_support_finding_count"] == 1 - assert docket.targets[0].target_type is ( - HumanReviewDocketTargetType.EVIDENCE_SUPPORT_FINDING - ) + assert docket.targets[0].target_type is (HumanReviewDocketTargetType.EVIDENCE_SUPPORT_FINDING) assert docket.targets[0].requires_decision() is True diff --git a/tests/test_human_review_gateway.py b/tests/test_human_review_gateway.py index bff3d04..d8d5cf8 100644 --- a/tests/test_human_review_gateway.py +++ b/tests/test_human_review_gateway.py @@ -196,8 +196,6 @@ def test_human_review_submission_result_payload_and_digest_are_stable() -> None: assert payload["before_stage"] == RunStage.HUMAN_REVIEW.value assert payload["next_stage"] == RunStage.EXECUTION_PLANNING.value - assert payload["decision_status"] == ( - HumanReviewDecisionStatus.APPROVED_FOR_EXECUTION.value - ) + assert payload["decision_status"] == (HumanReviewDecisionStatus.APPROVED_FOR_EXECUTION.value) assert first.digest() == second.digest() assert first.receipt.digest() == second.receipt.digest() diff --git a/tests/test_human_review_handoff.py b/tests/test_human_review_handoff.py index bedacf8..be679f0 100644 --- a/tests/test_human_review_handoff.py +++ b/tests/test_human_review_handoff.py @@ -91,17 +91,17 @@ def test_human_review_handoff_coordinator_preserves_custom_authority_note() -> N assert result.bundle.packet.authority_note == ( "Operator must approve the exact bounded action." ) - assert result.ledger_entry.authority_note == ( - "Operator must approve the exact bounded action." - ) - assert result.receipt.authority_note == ( - "Operator must approve the exact bounded action." - ) + assert result.ledger_entry.authority_note == ("Operator must approve the exact bounded action.") + assert result.receipt.authority_note == ("Operator must approve the exact bounded action.") def test_human_review_handoff_coordinator_counts_mixed_targets() -> None: - state = _state().with_action(_review_action()).with_evidence_support_finding( - _unsupported_finding(), + state = ( + _state() + .with_action(_review_action()) + .with_evidence_support_finding( + _unsupported_finding(), + ) ) result = HumanReviewHandoffCoordinator.create().handoff( diff --git a/tests/test_human_review_reentry_audit.py b/tests/test_human_review_reentry_audit.py index a6be3b6..aec2377 100644 --- a/tests/test_human_review_reentry_audit.py +++ b/tests/test_human_review_reentry_audit.py @@ -12,10 +12,10 @@ from ix_sally.human_review_reentry import HumanReviewReentryStatus from ix_sally.human_review_reentry_audit import ( HumanReviewReentryAuditFinding, + HumanReviewReentryAuditor, HumanReviewReentryAuditReport, HumanReviewReentryAuditSeverity, HumanReviewReentryAuditStatus, - HumanReviewReentryAuditor, ) from ix_sally.human_review_reentry_coordination import HumanReviewReentryCoordinator from ix_sally.human_review_workflow import HumanReviewWorkflowKit diff --git a/tests/test_human_review_reentry_audit_ledger.py b/tests/test_human_review_reentry_audit_ledger.py index ce1a790..76f076b 100644 --- a/tests/test_human_review_reentry_audit_ledger.py +++ b/tests/test_human_review_reentry_audit_ledger.py @@ -11,8 +11,8 @@ from ix_sally.human_review_gateway import HumanReviewDecisionStatus from ix_sally.human_review_reentry import HumanReviewReentryStatus from ix_sally.human_review_reentry_audit import ( - HumanReviewReentryAuditStatus, HumanReviewReentryAuditor, + HumanReviewReentryAuditStatus, ) from ix_sally.human_review_reentry_audit_ledger import ( HumanReviewReentryAuditLedger, @@ -125,9 +125,7 @@ def test_reentry_audit_ledger_appends_report_at_next_sequence() -> None: assert updated.passed_entries() == (latest,) assert updated.failed_entries() == () assert updated.entries_for_stage(RunStage.FORGE_DISPATCH) == (latest,) - assert updated.entries_for_reentry_status(HumanReviewReentryStatus.ADVANCED) == ( - latest, - ) + assert updated.entries_for_reentry_status(HumanReviewReentryStatus.ADVANCED) == (latest,) def test_reentry_audit_ledger_tracks_waiting_audit_report() -> None: diff --git a/tests/test_human_review_reentry_coordination.py b/tests/test_human_review_reentry_coordination.py index 0bd2379..12633fa 100644 --- a/tests/test_human_review_reentry_coordination.py +++ b/tests/test_human_review_reentry_coordination.py @@ -137,9 +137,7 @@ def test_reentry_coordination_payload_links_resume_reentry_and_workflow() -> Non assert payload["resume_operation_digest"] == resume.digest().value assert payload["reentry_result_digest"] == result.reentry_result.digest().value - assert payload["workflow_operation_digest"] == ( - result.workflow_operation.digest().value - ) + assert payload["workflow_operation_digest"] == (result.workflow_operation.digest().value) assert payload["recorded_reentry"] is True assert payload["reentry_count"] == 1 assert receipt_payload["recorded_reentry"] is True diff --git a/tests/test_human_review_resolution.py b/tests/test_human_review_resolution.py index 0899860..2126ea0 100644 --- a/tests/test_human_review_resolution.py +++ b/tests/test_human_review_resolution.py @@ -129,8 +129,12 @@ def test_resolution_audit_marks_evidence_card_manual_investigation() -> None: def test_resolution_audit_combines_resolved_and_manual_cards() -> None: - state = _state().with_action(_review_action()).with_evidence_support_finding( - _unsupported_finding(), + state = ( + _state() + .with_action(_review_action()) + .with_evidence_support_finding( + _unsupported_finding(), + ) ) action = state.actions.human_review_actions()[0] bundle = HumanReviewBundleAssembler.create().assemble(state=state) diff --git a/tests/test_human_review_workflow.py b/tests/test_human_review_workflow.py index 900c721..da409d1 100644 --- a/tests/test_human_review_workflow.py +++ b/tests/test_human_review_workflow.py @@ -8,7 +8,6 @@ from ix_sally.contracts import AutonomyContract, AutonomyMode from ix_sally.digest import DigestRecord from ix_sally.foundation import FoundationError -from ix_sally.human_review_control_plane import HumanReviewControlPlaneState from ix_sally.human_review_control_plane_coordinator import ( HumanReviewControlPlaneOperationKind, ) diff --git a/tests/test_human_review_workflow_audited_reentry.py b/tests/test_human_review_workflow_audited_reentry.py index 34bc569..17d912b 100644 --- a/tests/test_human_review_workflow_audited_reentry.py +++ b/tests/test_human_review_workflow_audited_reentry.py @@ -99,9 +99,7 @@ def test_workflow_kit_records_audited_reentry_operation() -> None: audited_reentry_result=audited_reentry, ) - assert operation.receipt.workflow_stage is ( - HumanReviewWorkflowStage.AUDITED_REENTRY_RECORDED - ) + assert operation.receipt.workflow_stage is (HumanReviewWorkflowStage.AUDITED_REENTRY_RECORDED) assert operation.operation_kind() is ( HumanReviewControlPlaneOperationKind.AUDITED_REENTRY_RECORDED ) @@ -110,9 +108,7 @@ def test_workflow_kit_records_audited_reentry_operation() -> None: assert operation.control_plane.reentry_count() == 1 assert operation.control_plane.reentry_audit_count() == 1 assert operation.control_plane.audited_reentry_count() == 1 - assert operation.report.status is ( - HumanReviewControlPlaneReportStatus.AUDITED_REENTRY_ACCEPTED - ) + assert operation.report.status is (HumanReviewControlPlaneReportStatus.AUDITED_REENTRY_ACCEPTED) assert operation.report.audited_reentry_recorded() is True assert operation.report.audited_reentry_accepted() is True assert operation.report.requires_operator_attention() is False @@ -145,9 +141,7 @@ def test_workflow_audited_reentry_payload_links_result_and_report() -> None: payload = operation.to_payload() report_payload = operation.report.to_payload() - assert payload["workflow_stage"] == ( - HumanReviewWorkflowStage.AUDITED_REENTRY_RECORDED.value - ) + assert payload["workflow_stage"] == (HumanReviewWorkflowStage.AUDITED_REENTRY_RECORDED.value) assert payload["operation_kind"] == ( HumanReviewControlPlaneOperationKind.AUDITED_REENTRY_RECORDED.value ) diff --git a/tests/test_human_review_workflow_complete_reentry.py b/tests/test_human_review_workflow_complete_reentry.py index b58ba24..2b10f7a 100644 --- a/tests/test_human_review_workflow_complete_reentry.py +++ b/tests/test_human_review_workflow_complete_reentry.py @@ -99,9 +99,7 @@ def test_workflow_kit_records_complete_reentry_operation() -> None: complete_reentry_result=complete_reentry, ) - assert operation.receipt.workflow_stage is ( - HumanReviewWorkflowStage.COMPLETE_REENTRY_RECORDED - ) + assert operation.receipt.workflow_stage is (HumanReviewWorkflowStage.COMPLETE_REENTRY_RECORDED) assert operation.operation_kind() is ( HumanReviewControlPlaneOperationKind.COMPLETE_REENTRY_RECORDED ) @@ -146,9 +144,7 @@ def test_workflow_complete_reentry_payload_links_result_and_report() -> None: payload = operation.to_payload() report_payload = operation.report.to_payload() - assert payload["workflow_stage"] == ( - HumanReviewWorkflowStage.COMPLETE_REENTRY_RECORDED.value - ) + assert payload["workflow_stage"] == (HumanReviewWorkflowStage.COMPLETE_REENTRY_RECORDED.value) assert payload["operation_kind"] == ( HumanReviewControlPlaneOperationKind.COMPLETE_REENTRY_RECORDED.value ) diff --git a/tests/test_human_review_workflow_complete_reentry_closeout.py b/tests/test_human_review_workflow_complete_reentry_closeout.py index 914460d..94253eb 100644 --- a/tests/test_human_review_workflow_complete_reentry_closeout.py +++ b/tests/test_human_review_workflow_complete_reentry_closeout.py @@ -97,9 +97,7 @@ def _complete_result(max_steps: int = 1): def test_workflow_kit_records_complete_reentry_closeout_operation() -> None: complete_reentry = _complete_result(max_steps=1) - closeout_report = CompleteHumanReviewReentryCloseoutReport.from_result( - complete_reentry - ) + closeout_report = CompleteHumanReviewReentryCloseoutReport.from_result(complete_reentry) operation = HumanReviewWorkflowKit.create().record_complete_reentry_closeout( run_state=complete_reentry.state, @@ -125,9 +123,7 @@ def test_workflow_kit_records_complete_reentry_closeout_operation() -> None: def test_workflow_kit_records_waiting_complete_reentry_closeout_status() -> None: complete_reentry = _complete_result(max_steps=3) - closeout_report = CompleteHumanReviewReentryCloseoutReport.from_result( - complete_reentry - ) + closeout_report = CompleteHumanReviewReentryCloseoutReport.from_result(complete_reentry) operation = HumanReviewWorkflowKit.create().record_complete_reentry_closeout( run_state=complete_reentry.state, @@ -147,9 +143,7 @@ def test_workflow_kit_records_waiting_complete_reentry_closeout_status() -> None def test_workflow_complete_reentry_closeout_payload_links_report() -> None: complete_reentry = _complete_result(max_steps=1) - closeout_report = CompleteHumanReviewReentryCloseoutReport.from_result( - complete_reentry - ) + closeout_report = CompleteHumanReviewReentryCloseoutReport.from_result(complete_reentry) operation = HumanReviewWorkflowKit.create().record_complete_reentry_closeout( run_state=complete_reentry.state, @@ -177,9 +171,7 @@ def test_workflow_complete_reentry_closeout_payload_links_report() -> None: def test_workflow_complete_reentry_closeout_rejects_mismatched_state() -> None: complete_reentry = _complete_result(max_steps=1) - closeout_report = CompleteHumanReviewReentryCloseoutReport.from_result( - complete_reentry - ) + closeout_report = CompleteHumanReviewReentryCloseoutReport.from_result(complete_reentry) with pytest.raises(FoundationError, match="run state must match report"): HumanReviewWorkflowKit.create().record_complete_reentry_closeout( @@ -191,9 +183,7 @@ def test_workflow_complete_reentry_closeout_rejects_mismatched_state() -> None: def test_workflow_complete_reentry_closeout_rejects_mismatched_plane() -> None: complete_reentry = _complete_result(max_steps=1) - closeout_report = CompleteHumanReviewReentryCloseoutReport.from_result( - complete_reentry - ) + closeout_report = CompleteHumanReviewReentryCloseoutReport.from_result(complete_reentry) with pytest.raises(FoundationError, match="closeout must match current"): HumanReviewWorkflowKit.create().record_complete_reentry_closeout( diff --git a/tests/test_human_review_workflow_reentry.py b/tests/test_human_review_workflow_reentry.py index 1c6f939..a2f0240 100644 --- a/tests/test_human_review_workflow_reentry.py +++ b/tests/test_human_review_workflow_reentry.py @@ -93,15 +93,11 @@ def test_workflow_kit_records_reentry_operation() -> None: operation = HumanReviewWorkflowKit.create().record_reentry(reentry_result=reentry) assert operation.receipt.workflow_stage is HumanReviewWorkflowStage.REENTRY_RECORDED - assert operation.operation_kind() is ( - HumanReviewControlPlaneOperationKind.REENTRY_RECORDED - ) + assert operation.operation_kind() is (HumanReviewControlPlaneOperationKind.REENTRY_RECORDED) assert operation.require_reentry() == reentry assert operation.run_state == reentry.state assert operation.control_plane.reentry_count() == 1 - assert operation.report.status is ( - HumanReviewControlPlaneReportStatus.REENTRY_RECORDED - ) + assert operation.report.status is (HumanReviewControlPlaneReportStatus.REENTRY_RECORDED) assert operation.report.has_reentry() is True assert operation.report.reentry_recorded() is True assert operation.report.requires_operator_attention() is False @@ -144,9 +140,7 @@ def test_workflow_reentry_payload_links_report_and_operation() -> None: assert payload["operation_kind"] == ( HumanReviewControlPlaneOperationKind.REENTRY_RECORDED.value ) - assert payload["report_status"] == ( - HumanReviewControlPlaneReportStatus.REENTRY_RECORDED.value - ) + assert payload["report_status"] == (HumanReviewControlPlaneReportStatus.REENTRY_RECORDED.value) assert payload["reentry_count"] == 1 assert report_payload["latest_reentry_digest"] == ( operation.control_plane.latest_reentry_digest() diff --git a/tests/test_human_review_workflow_reentry_audit.py b/tests/test_human_review_workflow_reentry_audit.py index 42fdba1..4c13f1e 100644 --- a/tests/test_human_review_workflow_reentry_audit.py +++ b/tests/test_human_review_workflow_reentry_audit.py @@ -101,9 +101,7 @@ def test_workflow_kit_records_reentry_audit_operation() -> None: control_plane=coordination.control_plane, ) - assert operation.receipt.workflow_stage is ( - HumanReviewWorkflowStage.REENTRY_AUDIT_RECORDED - ) + assert operation.receipt.workflow_stage is (HumanReviewWorkflowStage.REENTRY_AUDIT_RECORDED) assert operation.operation_kind() is ( HumanReviewControlPlaneOperationKind.REENTRY_AUDIT_RECORDED ) @@ -149,9 +147,7 @@ def test_workflow_reentry_audit_payload_links_report_and_operation() -> None: payload = operation.to_payload() report_payload = operation.report.to_payload() - assert payload["workflow_stage"] == ( - HumanReviewWorkflowStage.REENTRY_AUDIT_RECORDED.value - ) + assert payload["workflow_stage"] == (HumanReviewWorkflowStage.REENTRY_AUDIT_RECORDED.value) assert payload["operation_kind"] == ( HumanReviewControlPlaneOperationKind.REENTRY_AUDIT_RECORDED.value ) diff --git a/tests/test_memory.py b/tests/test_memory.py index adb99c2..5a4d229 100644 --- a/tests/test_memory.py +++ b/tests/test_memory.py @@ -15,9 +15,7 @@ def test_memory_record_normalizes_content_and_generates_id() -> None: content=" Failed repair pattern should be quarantined. ", ) - assert record.memory_id.value == ( - "ix-mnemosyne-2-failed-repair-pattern-should-be-quarantined" - ) + assert record.memory_id.value == ("ix-mnemosyne-2-failed-repair-pattern-should-be-quarantined") assert record.content == "Failed repair pattern should be quarantined." assert record.status is MemoryStatus.CANDIDATE assert record.is_truth_claim is False @@ -195,6 +193,7 @@ def test_memory_ledger_digest_changes_when_memory_status_changes() -> None: reason="Human boundary rejected this memory.", ) - assert MemoryLedger.create((candidate,)).digest().value != MemoryLedger.create( - (rejected,) - ).digest().value + assert ( + MemoryLedger.create((candidate,)).digest().value + != MemoryLedger.create((rejected,)).digest().value + ) diff --git a/tests/test_module_imports.py b/tests/test_module_imports.py index 11af747..e71f659 100644 --- a/tests/test_module_imports.py +++ b/tests/test_module_imports.py @@ -55,8 +55,7 @@ def test_runtime_modules_import_in_multiple_orders() -> None: ) if completed.returncode != 0: failures.append( - f"order {order_index}: " - f"{completed.stderr.strip() or completed.stdout.strip()}" + f"order {order_index}: {completed.stderr.strip() or completed.stdout.strip()}" ) assert not failures, "\n".join(failures) diff --git a/tests/test_package_smoke.py b/tests/test_package_smoke.py index 687bef0..e1dc000 100644 --- a/tests/test_package_smoke.py +++ b/tests/test_package_smoke.py @@ -5,9 +5,8 @@ import os from pathlib import Path -import pytest - import package_smoke +import pytest def test_isolated_environment_removes_python_path_overrides( @@ -35,9 +34,7 @@ def test_virtual_environment_paths_match_platform() -> None: environment_root / "Scripts" / "ix-sally.exe" ) else: - assert package_smoke._venv_python(environment_root) == ( - environment_root / "bin" / "python" - ) + assert package_smoke._venv_python(environment_root) == (environment_root / "bin" / "python") assert package_smoke._venv_console_script(environment_root) == ( environment_root / "bin" / "ix-sally" ) diff --git a/tests/test_repository_check.py b/tests/test_repository_check.py index 74288d2..374c210 100644 --- a/tests/test_repository_check.py +++ b/tests/test_repository_check.py @@ -37,9 +37,12 @@ def test_repository_integrity_passes_for_current_checkout() -> None: """The current repository must satisfy every cross-platform invariant.""" repository_root = Path(repository_check.__file__).resolve().parent - assert repository_check.repository_violations( - repository_root=repository_root, - ) == () + assert ( + repository_check.repository_violations( + repository_root=repository_root, + ) + == () + ) def test_repository_integrity_detects_case_collision( @@ -47,11 +50,26 @@ def test_repository_integrity_detects_case_collision( ) -> None: """Paths differing only by case must be rejected for Windows safety.""" _minimal_repository(tmp_path) - (tmp_path / "tests" / "sample.py").write_text( - "VALUE = 1\n", - encoding="utf-8", + paths = ( + tmp_path / "tests" / "sample.py", + tmp_path / "tests" / "Sample.py", + ) + + violations = repository_check._case_collision_violations( + repository_root=tmp_path, + paths=paths, ) - (tmp_path / "tests" / "Sample.py").write_text( + + assert any(violation.rule == "case-collision" for violation in violations) + + +def test_repository_integrity_detects_invalid_module_case( + tmp_path: Path, +) -> None: + """Mixed-case Python module names must be rejected cross-platform.""" + _minimal_repository(tmp_path) + module = tmp_path / "tests" / "Sample.py" + module.write_text( "VALUE = 2\n", encoding="utf-8", ) @@ -60,14 +78,7 @@ def test_repository_integrity_detects_case_collision( repository_root=tmp_path, ) - assert any( - violation.rule == "case-collision" - for violation in violations - ) - assert any( - violation.rule == "python-module-name" - for violation in violations - ) + assert any(violation.rule == "python-module-name" for violation in violations) def test_repository_integrity_detects_missing_package_marker( @@ -86,11 +97,14 @@ def test_repository_integrity_detects_missing_package_marker( repository_root=tmp_path, ) - assert repository_check.RepositoryViolation( - rule="package-marker", - path="src/ix_sally/nested", - detail="Python package directory is missing __init__.py", - ) in violations + assert ( + repository_check.RepositoryViolation( + rule="package-marker", + path="src/ix_sally/nested", + detail="Python package directory is missing __init__.py", + ) + in violations + ) def test_repository_integrity_detects_invalid_python_source( @@ -109,8 +123,7 @@ def test_repository_integrity_detects_invalid_python_source( ) assert any( - violation.rule == "python-syntax" - and violation.path == "src/ix_sally/invalid.py" + violation.rule == "python-syntax" and violation.path == "src/ix_sally/invalid.py" for violation in violations ) @@ -130,11 +143,14 @@ def test_repository_integrity_detects_windows_reserved_name( repository_root=tmp_path, ) - assert repository_check.RepositoryViolation( - rule="windows-reserved-name", - path="tests/con.py", - detail="component 'con.py' is reserved on Windows", - ) in violations + assert ( + repository_check.RepositoryViolation( + rule="windows-reserved-name", + path="tests/con.py", + detail="component 'con.py' is reserved on Windows", + ) + in violations + ) def test_repository_check_main_reports_summary( diff --git a/tests/test_stage_readiness.py b/tests/test_stage_readiness.py index f4249df..d34b0ad 100644 --- a/tests/test_stage_readiness.py +++ b/tests/test_stage_readiness.py @@ -176,11 +176,15 @@ def test_stage_snapshot_routes_human_review_before_other_work() -> None: human_review_note="Human boundary must approve this action.", ) reviewed_action = action.with_authority_decision(decision) - state = _state().with_action(reviewed_action).with_claim( - ClaimRecord.create( - cycle=1, - author=AgentRole.SALLY, - statement="Forge tests passed.", + state = ( + _state() + .with_action(reviewed_action) + .with_claim( + ClaimRecord.create( + cycle=1, + author=AgentRole.SALLY, + statement="Forge tests passed.", + ) ) ) snapshot = RunStageSnapshot.from_state(state) diff --git a/tests/test_state.py b/tests/test_state.py index 52933e2..28d5e6e 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -124,10 +124,7 @@ def test_run_state_appends_artifact_claim_evidence_and_memory() -> None: ) updated = ( - state.with_artifact(artifact) - .with_claim(claim) - .with_evidence(evidence) - .with_memory(memory) + state.with_artifact(artifact).with_claim(claim).with_evidence(evidence).with_memory(memory) ) assert len(updated.artifacts.artifacts) == 1 @@ -179,10 +176,7 @@ def test_run_state_payload_records_ledger_counts_and_digests() -> None: assert payload["claim_ledger_digest"] == state.claims.digest().value assert payload["evidence_ledger_digest"] == state.evidence.digest().value assert payload["memory_ledger_digest"] == state.memory.digest().value - assert ( - payload["authority_decision_ledger_digest"] - == state.authority_decisions.digest().value - ) + assert payload["authority_decision_ledger_digest"] == state.authority_decisions.digest().value assert payload["cycle_ledger_digest"] == state.cycles.digest().value assert payload["event_count"] == 1 assert payload["artifact_count"] == 0 diff --git a/tests/test_state_audit.py b/tests/test_state_audit.py index b59f5f0..203dd2e 100644 --- a/tests/test_state_audit.py +++ b/tests/test_state_audit.py @@ -17,9 +17,9 @@ from ix_sally.state import NinefoldRunState from ix_sally.state_audit import ( StateAuditFinding, + StateAuditor, StateAuditReport, StateAuditSeverity, - StateAuditor, ) @@ -210,10 +210,7 @@ def test_state_auditor_blocks_on_unsupported_evidence_findings() -> None: report = StateAuditor().audit(state) assert report.ready_for_close() is False - assert any( - finding.reference == "evidence_support.human_review" - for finding in report.findings - ) + assert any(finding.reference == "evidence_support.human_review" for finding in report.findings) def test_state_audit_digest_changes_when_findings_change() -> None: diff --git a/tests/test_state_audit_forge_result_count.py b/tests/test_state_audit_forge_result_count.py index 00e94ee..ad075da 100644 --- a/tests/test_state_audit_forge_result_count.py +++ b/tests/test_state_audit_forge_result_count.py @@ -57,8 +57,5 @@ def test_state_auditor_does_not_warn_when_dispatched_item_has_matching_result() report = StateAuditor().audit(state) - assert not any( - finding.reference == "execution_queue.dispatched" - for finding in report.findings - ) + assert not any(finding.reference == "execution_queue.dispatched" for finding in report.findings) assert report.ready_for_close() is True diff --git a/tests/test_version.py b/tests/test_version.py index 985e3d2..f65b8ec 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -7,8 +7,9 @@ import tomllib from pathlib import Path -import ix_sally import package_smoke + +import ix_sally from ix_sally.session_baseline import ( session_one_baseline_payload, ) @@ -25,40 +26,25 @@ def test_package_root_exports_canonical_version() -> None: def test_runtime_baseline_uses_canonical_version() -> None: """Runtime baseline identity must come from the version module.""" - assert ( - session_one_baseline_payload()["version"] - == __version__ - ) + assert session_one_baseline_payload()["version"] == __version__ def test_project_metadata_uses_dynamic_version_source() -> None: """Setuptools must derive wheel metadata from the version module.""" repository_root = Path(__file__).resolve().parents[1] - project_data = tomllib.loads( - ( - repository_root - / "pyproject.toml" - ).read_text(encoding="utf-8") - ) + project_data = tomllib.loads((repository_root / "pyproject.toml").read_text(encoding="utf-8")) - assert project_data["project"]["dynamic"] == [ - "version" - ] - assert ( - project_data["tool"]["setuptools"]["dynamic"]["version"] - == { - "attr": "ix_sally.version.__version__", - } - ) + assert project_data["project"]["dynamic"] == ["version"] + assert project_data["tool"]["setuptools"]["dynamic"]["version"] == { + "attr": "ix_sally.version.__version__", + } def test_package_smoke_reads_canonical_source_version() -> None: """Installed-package verification must use the source version.""" repository_root = Path(__file__).resolve().parents[1] - assert package_smoke._source_version( - repository_root=repository_root - ) == __version__ + assert package_smoke._source_version(repository_root=repository_root) == __version__ def test_version_module_is_dependency_neutral() -> None: