Metadata-Version: 2.4
Name: yasarda
Version: 0.4.0
Summary: Reviewed plans, guarded repository file changes, durable receipts, and a public recovery-status API.
Keywords: codemod,remediation,mcp,repository
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Version Control :: Git
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: jsonschema<5,>=4.23
Provides-Extra: mcp
Provides-Extra: dev
Requires-Dist: pytest<10,>=8; extra == "dev"

# Yasarda 0.4.0

**Reviewed plans. Guarded file changes. Durable receipts.**

A local tool with a Python SDK, CLI, and MCP stdio interface over one engine.
No account, cloud service, model API, GitHub dependency, or project-specific code.

## Install

```sh
pip install yasarda
yasarda --version
```

From the source project: `python -m pip install .`.
Python 3.10+ is declared. The runtime dependency is `jsonschema>=4.23,<5`.
MCP stdio is included in the base installation; `yasarda[mcp]` remains an empty
compatibility extra. There is no separate transport dependency.

Guarded mutation currently requires supported POSIX local filesystems for both
the target repository and journal state. Yasarda refuses known macOS cloud-synced locations (including iCloud Drive and
`~/Library/CloudStorage`) and known Linux remote/FUSE mounts by default. The
host-only `--allow-unsupported-filesystem` override exists for deliberate testing
or recovery, but does not make remote filesystem semantics safe. Linux and local
macOS storage are exercised in the accompanying report; Windows mutation and
power-loss behavior remain unsupported.

## The workflow

Put reviewed manifests in `<repository>/.yasarda/remediations/*.json`, or use an
external catalog with `--catalog /absolute/catalog/path`. Catalog creation is an
explicit authoring step; Yasarda does not install a sample over an existing file.

```sh
# Inspection and planning do not change repository files.
yasarda --repo /path/to/repo doctor
yasarda --repo /path/to/repo list
yasarda --repo /path/to/repo plan RP-001 --out /outside/repo/RP-001.plan.json

# Read the diff and approve this specific digest independently.
yasarda --repo /path/to/repo --mode approved-mutation \
  execute /outside/repo/RP-001.plan.json \
  --approve REVIEWED_64_CHARACTER_PLAN_ID --yes

yasarda --repo /path/to/repo verify RP-001
yasarda --repo /path/to/repo receipt REVIEWED_64_CHARACTER_PLAN_ID
```

Global options go before the subcommand. `--out` never overwrites an existing
artifact and must point outside the repository. An output directory must already
exist. A plan contains exact input bytes, output bytes, modes, dependency
manifests, Git identity, and a digest of its derived contents. Loading checks that
the stored transformations, diff, and digest agree. Execution does not replan.

A plan digest is an integrity identifier, **not a signature or proof of human
approval**. The caller/host must independently decide whether to authorize it.

For trusted, immediate local use, `yasarda --repo /path apply RP-001 --yes` plans
and authorizes that plan in one invocation. It is not the deferred-review path.
Without `--yes`, this command is inert. An explicit restrictive `--mode` wins
even when `--yes` is present.

A changed input, changed branch/HEAD, unknown file state, unresolved prerequisite,
unsupported link, or conflicting scope stops execution. The plan is bound to its
absolute repository root. To use another checkout location, regenerate a plan
from the manifests and review the new digest; do not edit saved plan JSON.

If a repository is stored in iCloud Drive, copy or clone it to local storage before
executing a plan. Planning and structural verification remain available in the
cloud-synced location because they do not publish repository changes.

## Modes

| Mode | Repository behavior |
| --- | --- |
| `read-only` | Inspect, plan, and structurally verify; no mutation. |
| `proposal-only` | Same repository restrictions; proposals can be explicitly saved outside it. |
| `new-files-only` | Create absent files or recognize exact existing results; no changes to existing files. |
| `approved-mutation` | Execute explicitly approved plans within all remaining guards. |

Read/plan CLI commands default to proposal-only. MCP defaults to read-only.
The SDK retains a trusted-code convention: constructing `Repository(path)` and
calling `apply` is explicit caller authorization. Set `Policy(mode=...)` when
embedding Yasarda in a restricted worker; the documentation examples do so.
A Python caller with direct filesystem access is not sandboxed by a Python class.

Dirty Git worktrees are refused by default. The **host** can permit them using
`--allow-dirty`; exact input fingerprints remain mandatory. Git's index is never
updated by Yasarda. Bare Git object stores are not mutation targets.

## Manifest example

```json
{
  "schema_version": 1,
  "id": "RP-001",
  "version": 1,
  "title": "Replace the legacy greeting",
  "changes": [
    {
      "type": "replace_text",
      "path": "hello.txt",
      "before": "hello old world\n",
      "after": "hello new world\n",
      "count": 1
    }
  ],
  "verify": [
    {"type": "contains", "path": "hello.txt", "text": "hello new world\n"}
  ]
}
```

`yasarda schema` emits the same JSON Schema enforced by the loader. Unknown
fields, incorrect types, nonpositive counts, conflicting hashes, duplicate JSON
keys, unsupported schema versions, and unsafe paths are rejected.

The four primitives are `replace_text`, `replace_file`, `create_file`, and
`delete_file`. `replace_file` requires `expected_old_sha256`; the desired hash is
computed from `content`. `delete_file` requires the old hash. `create_file` only
creates an absent path or recognizes matching bytes/mode. New files default to
0644; an explicit integer `mode` is permitted.

Text replacements operate on UTF-8 token bytes, not newline-normalized file
strings. Unrelated bytes, CRLF, BOMs, final-newline state, and permission bits are
preserved. Counted text detection is conservative, not semantic code analysis.
For stronger old/new identity, add **both** `expected_old_sha256` and
`expected_new_sha256` to a text change. Empty replacement text requires both.

One transformation per path is deliberate. Duplicate paths, case-fold collisions,
and parent/child collisions are rejected before publication. To make several
edits to one file, author one exact final `replace_file` payload instead of a
sequence of loosely overlapping edits.

Optional `branches` and `depends_on` are enforced through all orchestrating APIs,
including `catalog[id].apply(repo)`. Missing prerequisites and cycles fail closed.
The staged result is also checked not to undo its own prerequisites.

## Verification means what it says

Supported structural checks: `exists`, `absent`, `contains`, `not_contains`, and
`sha256`. Every execution also checks the **exact planned final bytes and modes**.
`verify RP-001` checks the manifest's desired-state predicates and declared checks;
a historical receipt is not a fresh observation of today's files.

**Command checks do not run.** A manifest containing a legacy `command` check
fails staged verification before destination publication. Run builds, tests,
Instruments, benchmarks, or other executable checks in a separately authorized
environment. A successful structural check is not proof that an app is correct,
fast, accessible, or secure.

## Recovery and receipts

Before publication, Yasarda stores an operation journal outside the repository,
stages the scoped output in a private directory, verifies it, and rechecks the
live inputs. Cooperating Yasarda writers share an advisory lock. Publication is
conditional and per-file; new-file publication does not clobber an existing path.

```sh
# Recover an interrupted operation to its original file state.
yasarda --repo /path/to/repo recover PLAN_ID --approve PLAN_ID --yes

# Explicitly undo a completed operation, provided its outputs have not diverged.
yasarda --repo /path/to/repo rollback PLAN_ID --approve PLAN_ID --yes
```

Repeating execution of the **same saved plan** returns its verified receipt only
when the result still matches. It does not apply the transformation twice. A
rolled-back or failed operation needs a fresh reviewed plan, not a forced retry.
An interrupted operation blocks further publication until recovery is resolved.
`doctor` reports pending operation IDs.

Recovery checks all affected files before restoring any. Unknown/newer content
or a changed Git branch/HEAD causes refusal, not overwriting. Recoverable original
bytes and modes remain in the journal. Empty directories created by an operation
are removed on rollback; directories containing somebody else's additions remain.
Receipts distinguish staged, published, and rollback verification.

Default state location:
`~/.local/state/yasarda/<repository-root-digest>/<plan-id>.json`.
Use `--state-dir` or host environment `YASARDA_STATE_DIR` to choose another private
location **outside** the repository. State directories are 0700; journals and
exported plans are 0600. Journals and plans contain source bytes; treat them as
sensitive, keep them while recovery may be needed, and manage retention yourself.
Receipts returned through the API omit those embedded source payloads.
Known cloud/remote filesystem refusal applies to this state path as well as the
repository because its lock and journal durability are part of recovery safety.

## Vinyasar supervisor boundary

Yasarda 0.3.0 adds two narrow read-only SDK methods for the separately packaged
Vinyasar automation layer:

```python
engine.pending_operations()             # exact IDs requiring recovery
repository.mutation_filesystem_issue()  # reason mutation storage is unsupported
```

Vinyasar calls these methods plus the existing `Engine.recover()` API. It does
not read journal files or call transaction internals. Yasarda remains the only
component that stages, publishes, restores, and verifies repository bytes.

## Python SDK

```python
from pathlib import Path
from yasarda import Catalog, Engine, Plan, Policy, Repository

root = Path("/path/to/repo")
repo = Repository(root, policy=Policy(mode="proposal-only"))
catalog = Catalog.load(root / ".yasarda/remediations")
plan = catalog.plan("RP-001", repo)
print(plan.diff)
plan.save("/outside/repo/reviewed.plan.json")

# Later, potentially in a new process, after independent review:
reviewed = Plan.load("/outside/repo/reviewed.plan.json")
executor = Repository(root, policy=Policy(mode="approved-mutation"))
receipt = Engine(executor).execute(reviewed, approval=reviewed.id)
assert receipt["state"] in {"verified", "already_satisfied"}
```

The approval in this example represents a trusted host decision. Do not copy
that auto-approval pattern into an untrusted agent endpoint. `Engine(...,
catalog=live_catalog)` additionally checks its supplied catalog snapshot against
the plan. A self-contained saved plan without a live catalog deliberately uses
its embedded, approved manifests and verifies dependencies against current files.

Errors have stable `code`, `message`, and `details` fields. CLI failures are JSON
on stderr; codes are 0 success, 2 blocked/invalid/I/O, 3 missing immediate-apply
confirmation, 4 verification failure, and 130 keyboard interruption.

## MCP stdio

Read-only host configuration:

```json
{
  "mcpServers": {
    "yasarda": {
      "command": "yasarda-mcp",
      "args": ["--repo", "/absolute/repository", "--mode", "read-only"]
    }
  }
}
```

To delegate execution, the host separately supplies the saved plan and approved
ID at process startup:

```sh
yasarda-mcp --repo /absolute/repository --mode approved-mutation \
  --plan-file /outside/repo/reviewed.plan.json --approve REVIEWED_PLAN_ID
```

There are **no tool arguments** for changing repository/catalog roots, mode,
dirty-worktree permission, command permission, or approvals. Read-only servers
omit mutation tools. A write-enabled server still rejects plans absent from its
host approval set. Completed-operation rollback additionally requires the host's
`--allow-rollback` flag; interrupted-operation recovery uses its approved ID.

Tools expose status, catalogs, inspection, planning, structural verification,
plan summaries, and receipts. Authorized hosts additionally expose execution and
recovery. Plans and manifests are available as `yasarda://` JSON resources.

The included transport is a bounded, synchronous tools/resources implementation
of MCP stdio for protocol versions 2025-11-25 and 2025-06-18. It negotiates the
version, validates input schemas, returns structured results/tool errors, and
keeps stdout protocol-only. It does not provide HTTP, OAuth, prompts, sampling,
tasks, subscriptions, or live mid-operation cancellation. Process interruption is
handled through the journal. See the test report for what was actually exercised;
this is not a claim of certification or testing in every MCP host application.

## Important limits

- This is not an OS security boundary, multi-file atomic filesystem, or semantic
  code-repair oracle. Keep noncooperating writers out of the destination while
  publishing. An agent with independent shell/filesystem access can bypass a tool.
- Regular singly-linked files only. Symlinks, hardlinks, devices, special
  permission bits, and directory transformations are rejected. Content and POSIX
  mode are tracked; ownership, ACLs, xattrs, and timestamps are not preserved as a
  complete metadata snapshot. Use an appropriate filesystem copy tool when those
  attributes matter.
- Protected paths include Git metadata, Yasarda configuration, credential
  directories, dotenv files, and root CI workflows. A trusted SDK host may supply
  a different `Policy`; MCP callers cannot.
- Default limits: 8 MiB per file, 24 MiB input snapshot, 200 scoped files. Generated
  outputs have an 8 MiB hard cap; serialized plans have a 64 MiB cap. MCP sessions
  hold at most 100 plans and 64 MiB of serialized plans. Oversized jobs must be
  deliberately split, not silently truncated.
- Local source artifacts only. No AST adapters, signed registries, network
  publication, sandboxed build runner, or hidden cloud dependencies.

## Tests and handoff

```sh
python -m pip install '.[dev]'
python -m pytest
```

The source includes the unmodified seven original tests and eight original audit
regressions, plus failure-injection, interruption, permission, saved-plan, and MCP
wire tests. See `docs/TEST_REPORT.md`, `docs/SAFETY.md`, `CHANGELOG.md`, and
`docs/HANDOFF.md` for evidence and precise limitations.
