intact 0.7.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
intact-0.7.1/PKG-INFO ADDED
@@ -0,0 +1,154 @@
1
+ Metadata-Version: 2.3
2
+ Name: intact
3
+ Version: 0.7.1
4
+ Summary: A reconciliation ladder that heals stale or drifted edits instead of failing them, with atomic multi-file transactions, patch ingestion, undo, durable anchors, and an LSP client — as a library, a CLI, and an MCP server.
5
+ Author: Robb Doering
6
+ Author-email: Robb Doering <robb@doering.ai>
7
+ Classifier: Programming Language :: Python :: 3.13
8
+ Classifier: Framework :: Pydantic :: 2
9
+ Classifier: Typing :: Typed
10
+ Requires-Dist: fastmcp>=3.4.0 ; extra == 'mcp'
11
+ Requires-Python: >=3.13
12
+ Project-URL: Homepage, https://gitlab.com/doering-ai/libs/tact
13
+ Project-URL: Source, https://gitlab.com/doering-ai/libs/tact
14
+ Project-URL: Issues, https://gitlab.com/doering-ai/libs/tact/-/issues
15
+ Project-URL: Documentation, https://intact.readthedocs.io
16
+ Provides-Extra: mcp
17
+ Description-Content-Type: text/markdown
18
+
19
+ # intact: _Self-Healing File Edits for Agents_
20
+
21
+ ![Pipeline Status](https://img.shields.io/gitlab/pipeline-status/doering-ai/libs/tact?branch=main) ![Test Coverage](https://img.shields.io/gitlab/pipeline-coverage/doering-ai/libs/tact?branch=main) [![License](https://img.shields.io/gitlab/license/doering-ai/libs/tact)](/LICENSE) [![Documentation](https://app.readthedocs.org/projects/intact/badge)](https://intact.readthedocs.io) ![Python Version](https://img.shields.io/pypi/pyversions/intact) [![PyPI Wheel](https://img.shields.io/pypi/wheel/intact)](https://pypi.org/project/intact) [![PyPI Types](https://img.shields.io/pypi/types/intact)](https://pypi.org/project/intact)
22
+
23
+ [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit) [![version](https://img.shields.io/badge/version-0.7.1-blue)](https://gitlab.com/doering-ai/corpus/-/blob/main/policies/versioning.md) [![pyrefly](https://img.shields.io/endpoint?url=https://pyrefly.org/badge.json)](https://github.com/facebook/pyrefly) [![ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
24
+
25
+ > A reconciliation ladder that heals stale or drifted edits instead of failing them, with atomic multi-file transactions, patch ingestion, undo, durable anchors, and an LSP client — as a library, a CLI, and an MCP server.
26
+
27
+ > [!NOTE] **Genesis.** `intact` was designed, built, reviewed, and shipped end-to-end by **Claude (Fable 5)** as an autonomous long-run engineering experiment — from mining two weeks of real agent-session ledgers for the failure class worth killing, through a fleet of builder/critic subagents and adversarial review rounds, to the benchmark, the extraction, and this release.
28
+ > The human operator set the charter, held the release gates, and found the name.
29
+ > The eval numbers below are measured on that same fleet's real failures — this package was built by the population it serves.
30
+
31
+ ______________________________________________________________________
32
+
33
+ ## The problem, measured
34
+
35
+ An independent audit of two weeks of this fleet's own Claude Code session ledgers found that the single largest *recurring* cost line wasn't a wrong answer or a slow model — it was harness friction: an agent's builtin `Edit` tool failing an exact-string match against text that had drifted underneath it (formatter passes, a stale read, whitespace normalization), 99 instances totaling roughly 190K tokens over the window, each one paid as a full read-diff-retry round trip.
36
+ `tact` exists to kill that class of failure at the source: it heals what's mechanical (whitespace, Unicode punctuation, indentation shift, small textual drift) through a symbolic, self-reporting reconciliation ladder, and it refuses loudly — never silently — the moment a match is genuinely ambiguous or absent, because a wrong guess in a file edit is worse than an honest failure.
37
+ It ships as a library, a CLI, and an MCP server, so the same reconciliation core backs whichever front end a given agent harness actually calls.
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ pip install intact
43
+ # or
44
+ uv add intact
45
+ ```
46
+
47
+ The CLI and library work with no extras.
48
+ The MCP server needs the `mcp` extra:
49
+
50
+ ```bash
51
+ uv add "intact[mcp]"
52
+ ```
53
+
54
+ ## 60-second quickstart
55
+
56
+ ### As a library
57
+
58
+ ```python
59
+ from tact import read_file, apply_edits, Edit
60
+
61
+ r = read_file('config.py')
62
+ apply_edits(
63
+ 'config.py',
64
+ [Edit(old='DEBUG = True', new='DEBUG = False')],
65
+ expected_sha256=r.sha256, # advisory -- a mismatch heals, it never blocks
66
+ )
67
+ ```
68
+
69
+ ### As a CLI
70
+
71
+ ```bash
72
+ # a builtin exact-match Edit just failed -- heal it in one round trip
73
+ uv run tact heal config.py --old "DEBUG = True" --new "DEBUG = False"
74
+
75
+ # or drive the whole read -> edit cycle through tact directly
76
+ uv run tact --json edit config.py --old "DEBUG = True" --new "DEBUG = False"
77
+ ```
78
+
79
+ ### As an MCP server
80
+
81
+ Register `tact` (adapt the project path for your own install):
82
+
83
+ ```bash
84
+ uv run --extra mcp --project /path/to/tact tact-mcp
85
+ ```
86
+
87
+ Then call `tact_edit`/`tact_heal`/`tact_definition`/… as MCP tools instead of the harness's builtin Read/Edit.
88
+ See `docs/howto.md` for per-harness registration snippets (Claude Code, Droid, Codex, Cursor).
89
+
90
+ ## The reconciliation ladder
91
+
92
+ `find_block` descends five rungs of mounting tolerance, stopping at the *finest* rung that yields a **unique** match:
93
+
94
+ | Rung | What it forgives | Example |
95
+ | -------- | --------------------------------------------------- | ---------------------------------------------- |
96
+ | `EXACT` | Nothing — verbatim line equality | `old_string` matches byte-for-byte |
97
+ | `CANON` | Unicode punctuation substitution | a curly `"quote"` pasted in for a straight one |
98
+ | `RSTRIP` | Trailing whitespace drift | a formatter stripped trailing spaces on save |
99
+ | `INDENT` | A uniform leading-whitespace shift | the block moved one indent level deeper |
100
+ | `FUZZY` | Small textual drift, bounded by per-line similarity | a stale line number after a nearby edit landed |
101
+
102
+ Two or more candidates at any rung is a **structured ambiguity refusal** (every candidate shown, resolved only by a `--near` hint); zero candidates at every rung is a **structured no-match refusal**.
103
+ Rungs 1-4 are true equivalence relations; rung 5 is a similarity ball, not transitive — which is exactly why ambiguity-handling concentrates there.
104
+ See `docs/explanation.md` for the full framing.
105
+
106
+ ## The numbers
107
+
108
+ Replayed against the fleet's own real edit-failure history (`tact.eval`, ledger-mined, no `near` hint supplied — any multi-candidate rung is a refusal, never a guess):
109
+
110
+ | Corpus | Cases | Healed | False heals | Correct refusals |
111
+ | ------------------------------------------ | ----- | ------ | ----------- | ---------------- |
112
+ | Public subset (this repo, `tests/`) | 59 | 43 | 0 | 1 |
113
+ | Full private fleet corpus (eval-of-record) | 111 | 68 | 0 | 1 |
114
+
115
+ The public subset is a privacy-reviewed 59-fixture slice of the full 121-fixture harvest (62 fixtures held back for operator-stack detail or private prose, not for reconciliation performance — see `tests/fixtures/ledger_eval/README.md` for the curation).
116
+ Cases that don't heal are mostly `no_match`, not misses: a large share are `modified_since_read` failures where the stored file snapshot legitimately predates the real edit, so a refusal there is reality reflected correctly, not a ladder weakness.
117
+
118
+ ## Features
119
+
120
+ - **`check`/`edit`** — resolve-then-atomically-write a batch of edits, with a dry-run twin (`check_edits`) that previews without touching the file.
121
+ - **`apply_many` transactions** — resolve every file in a batch before writing any of them; any failure blocks the whole transaction, nothing partial is ever written.
122
+ - **`heal`** — fix a failed builtin `Edit(path, old_string, new_string)` in one read-only round trip.
123
+ - **`undo`** — a content-addressed pre-image journal behind every write, with a redoable `undo` verb.
124
+ - **Durable anchors** — capture a block's position so it survives a session/compaction boundary; `resolve_anchor` re-finds it, biased by its own remembered index.
125
+ - **`delta`/`read_since`** — a cheap "what changed since this hash" re-read, backed by the undo journal's blob store.
126
+ - **`rename_symbol`** — LSP-computed cross-file renames, converted into one `apply_many` transaction (the one verb that is not LSP-optional).
127
+ - **`apply_patch`** — ingest a unified diff or a fenced old/new block pair straight into an `Edit` batch.
128
+ - **LSP navigation** — `def`/`refs`/`hover`/`sym`/`diag`, name-first, backed by a persistent, resilient pyrefly/ruff client.
129
+
130
+ ## Honest limits
131
+
132
+ `tact` matches line-by-line, so a paragraph reflow (a prose formatter that rewraps text across different line boundaries) is not a whitespace/indent variant it heals — expect a no-match refusal and a manual re-read there.
133
+ Ambiguity refusals need a `--near` hint or a more distinctive block; tact will not pick a near-tie for you.
134
+ `rename_symbol` is LSP-backed only — it never falls back to a textual find-replace, since that risks renaming the wrong binding, so it structurally refuses when no live LSP is available.
135
+ The undo journal keeps the newest 32 pre-images per file and is index-pruned, not globally garbage-collected — it is acceptable-loss state, not a full version history.
136
+
137
+ ## Provenance
138
+
139
+ The reconciliation ladder's canonicalization table and fuzzy-matching primitives in `tact/reconcile.py` are a port, with attribution, of parts of [`dirac`](https://github.com/dirac-run/dirac) (Apache-2.0, version 0.4.11) — see `NOTICE` and `reconcile.py`'s own module docstring for exactly what was ported and how it was adapted.
140
+
141
+ ## License
142
+
143
+ Apache-2.0.
144
+ See [`LICENSE`](LICENSE) and [`NOTICE`](NOTICE).
145
+
146
+ ______________________________________________________________________
147
+
148
+ ## Versioning
149
+
150
+ This project follows [workflow-gated semver]: the minor version records the highest-completed `py-workflow` step (`0.1.0`-`0.8.0`), with `0.9.0` (beta) gated on >=9 external users and `1.0.0` on a maintainer social guarantee.
151
+ The current version `0.7.0` means gate `7` is the highest cleared.
152
+ See the [normative rubric](https://gitlab.com/doering-ai/corpus/-/blob/main/policies/versioning.md) and the [overview](https://gitlab.com/doering-ai/corpus/-/blob/main/docs/versioning.md).
153
+
154
+ [workflow-gated semver]: https://gitlab.com/doering-ai/corpus/-/blob/main/docs/versioning.md
intact-0.7.1/README.md ADDED
@@ -0,0 +1,136 @@
1
+ # intact: _Self-Healing File Edits for Agents_
2
+
3
+ ![Pipeline Status](https://img.shields.io/gitlab/pipeline-status/doering-ai/libs/tact?branch=main) ![Test Coverage](https://img.shields.io/gitlab/pipeline-coverage/doering-ai/libs/tact?branch=main) [![License](https://img.shields.io/gitlab/license/doering-ai/libs/tact)](/LICENSE) [![Documentation](https://app.readthedocs.org/projects/intact/badge)](https://intact.readthedocs.io) ![Python Version](https://img.shields.io/pypi/pyversions/intact) [![PyPI Wheel](https://img.shields.io/pypi/wheel/intact)](https://pypi.org/project/intact) [![PyPI Types](https://img.shields.io/pypi/types/intact)](https://pypi.org/project/intact)
4
+
5
+ [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit) [![version](https://img.shields.io/badge/version-0.7.1-blue)](https://gitlab.com/doering-ai/corpus/-/blob/main/policies/versioning.md) [![pyrefly](https://img.shields.io/endpoint?url=https://pyrefly.org/badge.json)](https://github.com/facebook/pyrefly) [![ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
6
+
7
+ > A reconciliation ladder that heals stale or drifted edits instead of failing them, with atomic multi-file transactions, patch ingestion, undo, durable anchors, and an LSP client — as a library, a CLI, and an MCP server.
8
+
9
+ > [!NOTE] **Genesis.** `intact` was designed, built, reviewed, and shipped end-to-end by **Claude (Fable 5)** as an autonomous long-run engineering experiment — from mining two weeks of real agent-session ledgers for the failure class worth killing, through a fleet of builder/critic subagents and adversarial review rounds, to the benchmark, the extraction, and this release.
10
+ > The human operator set the charter, held the release gates, and found the name.
11
+ > The eval numbers below are measured on that same fleet's real failures — this package was built by the population it serves.
12
+
13
+ ______________________________________________________________________
14
+
15
+ ## The problem, measured
16
+
17
+ An independent audit of two weeks of this fleet's own Claude Code session ledgers found that the single largest *recurring* cost line wasn't a wrong answer or a slow model — it was harness friction: an agent's builtin `Edit` tool failing an exact-string match against text that had drifted underneath it (formatter passes, a stale read, whitespace normalization), 99 instances totaling roughly 190K tokens over the window, each one paid as a full read-diff-retry round trip.
18
+ `tact` exists to kill that class of failure at the source: it heals what's mechanical (whitespace, Unicode punctuation, indentation shift, small textual drift) through a symbolic, self-reporting reconciliation ladder, and it refuses loudly — never silently — the moment a match is genuinely ambiguous or absent, because a wrong guess in a file edit is worse than an honest failure.
19
+ It ships as a library, a CLI, and an MCP server, so the same reconciliation core backs whichever front end a given agent harness actually calls.
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ pip install intact
25
+ # or
26
+ uv add intact
27
+ ```
28
+
29
+ The CLI and library work with no extras.
30
+ The MCP server needs the `mcp` extra:
31
+
32
+ ```bash
33
+ uv add "intact[mcp]"
34
+ ```
35
+
36
+ ## 60-second quickstart
37
+
38
+ ### As a library
39
+
40
+ ```python
41
+ from tact import read_file, apply_edits, Edit
42
+
43
+ r = read_file('config.py')
44
+ apply_edits(
45
+ 'config.py',
46
+ [Edit(old='DEBUG = True', new='DEBUG = False')],
47
+ expected_sha256=r.sha256, # advisory -- a mismatch heals, it never blocks
48
+ )
49
+ ```
50
+
51
+ ### As a CLI
52
+
53
+ ```bash
54
+ # a builtin exact-match Edit just failed -- heal it in one round trip
55
+ uv run tact heal config.py --old "DEBUG = True" --new "DEBUG = False"
56
+
57
+ # or drive the whole read -> edit cycle through tact directly
58
+ uv run tact --json edit config.py --old "DEBUG = True" --new "DEBUG = False"
59
+ ```
60
+
61
+ ### As an MCP server
62
+
63
+ Register `tact` (adapt the project path for your own install):
64
+
65
+ ```bash
66
+ uv run --extra mcp --project /path/to/tact tact-mcp
67
+ ```
68
+
69
+ Then call `tact_edit`/`tact_heal`/`tact_definition`/… as MCP tools instead of the harness's builtin Read/Edit.
70
+ See `docs/howto.md` for per-harness registration snippets (Claude Code, Droid, Codex, Cursor).
71
+
72
+ ## The reconciliation ladder
73
+
74
+ `find_block` descends five rungs of mounting tolerance, stopping at the *finest* rung that yields a **unique** match:
75
+
76
+ | Rung | What it forgives | Example |
77
+ | -------- | --------------------------------------------------- | ---------------------------------------------- |
78
+ | `EXACT` | Nothing — verbatim line equality | `old_string` matches byte-for-byte |
79
+ | `CANON` | Unicode punctuation substitution | a curly `"quote"` pasted in for a straight one |
80
+ | `RSTRIP` | Trailing whitespace drift | a formatter stripped trailing spaces on save |
81
+ | `INDENT` | A uniform leading-whitespace shift | the block moved one indent level deeper |
82
+ | `FUZZY` | Small textual drift, bounded by per-line similarity | a stale line number after a nearby edit landed |
83
+
84
+ Two or more candidates at any rung is a **structured ambiguity refusal** (every candidate shown, resolved only by a `--near` hint); zero candidates at every rung is a **structured no-match refusal**.
85
+ Rungs 1-4 are true equivalence relations; rung 5 is a similarity ball, not transitive — which is exactly why ambiguity-handling concentrates there.
86
+ See `docs/explanation.md` for the full framing.
87
+
88
+ ## The numbers
89
+
90
+ Replayed against the fleet's own real edit-failure history (`tact.eval`, ledger-mined, no `near` hint supplied — any multi-candidate rung is a refusal, never a guess):
91
+
92
+ | Corpus | Cases | Healed | False heals | Correct refusals |
93
+ | ------------------------------------------ | ----- | ------ | ----------- | ---------------- |
94
+ | Public subset (this repo, `tests/`) | 59 | 43 | 0 | 1 |
95
+ | Full private fleet corpus (eval-of-record) | 111 | 68 | 0 | 1 |
96
+
97
+ The public subset is a privacy-reviewed 59-fixture slice of the full 121-fixture harvest (62 fixtures held back for operator-stack detail or private prose, not for reconciliation performance — see `tests/fixtures/ledger_eval/README.md` for the curation).
98
+ Cases that don't heal are mostly `no_match`, not misses: a large share are `modified_since_read` failures where the stored file snapshot legitimately predates the real edit, so a refusal there is reality reflected correctly, not a ladder weakness.
99
+
100
+ ## Features
101
+
102
+ - **`check`/`edit`** — resolve-then-atomically-write a batch of edits, with a dry-run twin (`check_edits`) that previews without touching the file.
103
+ - **`apply_many` transactions** — resolve every file in a batch before writing any of them; any failure blocks the whole transaction, nothing partial is ever written.
104
+ - **`heal`** — fix a failed builtin `Edit(path, old_string, new_string)` in one read-only round trip.
105
+ - **`undo`** — a content-addressed pre-image journal behind every write, with a redoable `undo` verb.
106
+ - **Durable anchors** — capture a block's position so it survives a session/compaction boundary; `resolve_anchor` re-finds it, biased by its own remembered index.
107
+ - **`delta`/`read_since`** — a cheap "what changed since this hash" re-read, backed by the undo journal's blob store.
108
+ - **`rename_symbol`** — LSP-computed cross-file renames, converted into one `apply_many` transaction (the one verb that is not LSP-optional).
109
+ - **`apply_patch`** — ingest a unified diff or a fenced old/new block pair straight into an `Edit` batch.
110
+ - **LSP navigation** — `def`/`refs`/`hover`/`sym`/`diag`, name-first, backed by a persistent, resilient pyrefly/ruff client.
111
+
112
+ ## Honest limits
113
+
114
+ `tact` matches line-by-line, so a paragraph reflow (a prose formatter that rewraps text across different line boundaries) is not a whitespace/indent variant it heals — expect a no-match refusal and a manual re-read there.
115
+ Ambiguity refusals need a `--near` hint or a more distinctive block; tact will not pick a near-tie for you.
116
+ `rename_symbol` is LSP-backed only — it never falls back to a textual find-replace, since that risks renaming the wrong binding, so it structurally refuses when no live LSP is available.
117
+ The undo journal keeps the newest 32 pre-images per file and is index-pruned, not globally garbage-collected — it is acceptable-loss state, not a full version history.
118
+
119
+ ## Provenance
120
+
121
+ The reconciliation ladder's canonicalization table and fuzzy-matching primitives in `tact/reconcile.py` are a port, with attribution, of parts of [`dirac`](https://github.com/dirac-run/dirac) (Apache-2.0, version 0.4.11) — see `NOTICE` and `reconcile.py`'s own module docstring for exactly what was ported and how it was adapted.
122
+
123
+ ## License
124
+
125
+ Apache-2.0.
126
+ See [`LICENSE`](LICENSE) and [`NOTICE`](NOTICE).
127
+
128
+ ______________________________________________________________________
129
+
130
+ ## Versioning
131
+
132
+ This project follows [workflow-gated semver]: the minor version records the highest-completed `py-workflow` step (`0.1.0`-`0.8.0`), with `0.9.0` (beta) gated on >=9 external users and `1.0.0` on a maintainer social guarantee.
133
+ The current version `0.7.0` means gate `7` is the highest cleared.
134
+ See the [normative rubric](https://gitlab.com/doering-ai/corpus/-/blob/main/policies/versioning.md) and the [overview](https://gitlab.com/doering-ai/corpus/-/blob/main/docs/versioning.md).
135
+
136
+ [workflow-gated semver]: https://gitlab.com/doering-ai/corpus/-/blob/main/docs/versioning.md
@@ -0,0 +1,233 @@
1
+ ###################
2
+ ### I. METADATA ###
3
+ ###################
4
+ [project]
5
+ name = "intact"
6
+ version = "0.7.1"
7
+ description = "A reconciliation ladder that heals stale or drifted edits instead of failing them, with atomic multi-file transactions, patch ingestion, undo, durable anchors, and an LSP client \u2014 as a library, a CLI, and an MCP server."
8
+ readme = "README.md"
9
+ authors = [{ name = "Robb Doering", email = "robb@doering.ai" }]
10
+ requires-python = ">=3.13"
11
+ classifiers = [
12
+ "Programming Language :: Python :: 3.13",
13
+ "Framework :: Pydantic :: 2",
14
+ "Typing :: Typed",
15
+ ]
16
+ # PyPI renders these as the sidebar "Project links".
17
+ urls.Homepage = "https://gitlab.com/doering-ai/libs/tact"
18
+ urls.Source = "https://gitlab.com/doering-ai/libs/tact"
19
+ urls.Issues = "https://gitlab.com/doering-ai/libs/tact/-/issues"
20
+ urls.Documentation = "https://intact.readthedocs.io"
21
+
22
+
23
+ ########################
24
+ ### II. DEPENDENCIES ###
25
+ ########################
26
+ dependencies = []
27
+
28
+ [project.optional-dependencies]
29
+ mcp = ["fastmcp>=3.4.0"]
30
+
31
+ [project.scripts]
32
+ tact = "tact.cli:main"
33
+ tact-mcp = "tact.mcp_server:main"
34
+
35
+ [dependency-groups]
36
+ dev = [
37
+ { include-group = "cli" },
38
+ { include-group = "eval" },
39
+ { include-group = "test" },
40
+ { include-group = "docs" },
41
+ ]
42
+
43
+ # Sub-groups for development dependencies
44
+ cli = ["bpython>=0.26", "better-exceptions>=0.3.3", "prek>=0.3.13"]
45
+ # Evaluation dependencies for type checking, linting, and code analysis
46
+ eval = ["ruff>=0.14.14", "pyrefly>=0.61.0"]
47
+ # Testing dependencies for unit tests, coverage, and test utilities
48
+ test = [
49
+ "coverage>=7.10.5",
50
+ "hypothesis>=6.140.0",
51
+ "pytest-asyncio>=1.1.0",
52
+ "pytest-cov>=6.2.1",
53
+ "pytest>=8.4.1",
54
+ "syrupy>=5.1.0",
55
+ ]
56
+ # Documentation dependencies for building and hosting project documentation
57
+ docs = [
58
+ "furo>=2025.12.19", # HTML Theme
59
+ "linkify-it-py>=2.0.3", # Nested sphinx dependency
60
+ "myst-parser>=4.0.1", # Support myst syntax
61
+ "sphinx>=8.2.3", # Documentation generator
62
+ "sphinx-hoverxref>=1.4.2", # Cross-referencing tool
63
+ "sphinx-last-updated-by-git>=0.3.8", # Git integration for last updated info
64
+ "sphinx-notfound-page>=1.1.0", # Custom 404 page for missing docs
65
+ ]
66
+
67
+ [tool.uv]
68
+ # This 10-day buffer is a critical cybersafety measure; do not remove without an alternative!
69
+ exclude-newer = "10 days"
70
+
71
+ [tool.uv.exclude-newer-package]
72
+ # Put any dependencies that you're working on/with in this list to exempt them from the buffer
73
+
74
+ #########################
75
+ ### III. BUILD SYSTEM ###
76
+ #########################
77
+ [build-system]
78
+ requires = ["uv_build>=0.11.7,<0.12"]
79
+ build-backend = "uv_build"
80
+ [tool.uv.build-backend]
81
+ module-name = "tact"
82
+ module-root = ""
83
+
84
+
85
+ #############################
86
+ ### IV. DEVELOPMENT TOOLS ###
87
+ #############################
88
+ # -------
89
+ # Testing
90
+ # -------
91
+ [tool.pytest]
92
+ testpaths = ["tests"]
93
+ markers = [
94
+ "integration: slower test exercising a real subprocess/service, not just logic",
95
+ ]
96
+
97
+ [tool.coverage.report]
98
+ # Regexes for lines to exclude from consideration
99
+ exclude_also = [
100
+ 'def __repr__',
101
+ 'if[- \w.]+\b(?i:debug):',
102
+ 'assert ',
103
+ 'raise (Assertion|NotImplemented)Error',
104
+ 'if 0:',
105
+ 'if __name__ == .__main__.:',
106
+ '@(abc\.)?abstractmethod',
107
+ '^([a-z]\w+?\.)?[A-Z][_A-Z]+(:[^=\n]+)? *=[^\n]+(?=\n\S)',
108
+ ]
109
+ ignore_errors = true
110
+
111
+ # ------
112
+ # Typing
113
+ # ------
114
+ [tool.pyrefly]
115
+ project-includes = ["tact/**/*.py*", "tests/**/*.py*"]
116
+ min-severity = 'warn'
117
+ python-version = "3.13"
118
+ check-unannotated-defs = true
119
+ permissive-ignores = true
120
+ search-path = ['.']
121
+
122
+ [tool.pyrefly.errors]
123
+ missing-source = false
124
+ implicit-any = false
125
+ unannotated-parameter = false
126
+ no-access = 'error'
127
+ unused-ignore = 'error'
128
+ unannotated-attribute = 'error'
129
+
130
+ # -------
131
+ # Linting
132
+ # -------
133
+ [tool.ruff]
134
+ indent-width = 4
135
+ line-length = 100
136
+
137
+ [tool.ruff.format]
138
+ quote-style = "single"
139
+ indent-style = "space"
140
+ skip-magic-trailing-comma = false
141
+ line-ending = "auto"
142
+ docstring-code-format = true
143
+ docstring-code-line-length = "dynamic"
144
+
145
+ [tool.ruff.lint]
146
+ select = [
147
+ "A", # builtins
148
+ "ASYNC", # async
149
+ "B", # bugbear
150
+ "C4", # comprehensions
151
+ "D", # PyDocStyle
152
+ "E", # PyCodeStyle errors
153
+ "F", # PyFlakes
154
+ "FA", # annotations
155
+ "FAST", # FastAPI
156
+ "ISC", # implicit-str-concat
157
+ "N", # Naming
158
+ "TC", # type checking
159
+ "NPY", # numpy
160
+ "PIE", # misc.
161
+ "PLE", # PyLint errors
162
+ "PTH", # use pathlib
163
+ "PERF", # performance
164
+ "PT", # pytest
165
+ "PD", # pandas
166
+ "RUF", # ruff
167
+ "SIM", # simplification
168
+ "UP", # upgrade 2.7 idioms
169
+ ]
170
+
171
+ ignore = [
172
+ # Bugbear
173
+ "B009",
174
+ "B905",
175
+
176
+ # Comprehensions
177
+ "C408",
178
+ "C413",
179
+
180
+ # PyCodeStyle
181
+ "E266",
182
+ "E731", # allow lambdas
183
+
184
+ # Naming
185
+ "N999",
186
+
187
+ # PyLint errors
188
+ "PT013",
189
+ "PT018",
190
+
191
+ # Ruff
192
+ "RUF022",
193
+
194
+ # Simplify
195
+ "SIM102", # combine nested ifs
196
+ "SIM114", # combine if branches
197
+ "SIM118",
198
+
199
+ # PyDocStyle
200
+ "D100", # docstrings for modules
201
+ "D104", # docstrings for packages
202
+ "D105", # docstrings for magic methods
203
+ "D410", # blank line after docstring sections
204
+ "D411", # blank line before docstring sections
205
+
206
+ # Upgrade
207
+ "UP007",
208
+
209
+ # Type Checking -- ported from corpus's own ignore (the moved code was written and reviewed
210
+ # under this convention there; corpus still carries it).
211
+ "TC001", # TYPE_CHECKING blocks in python3.8 (internal)
212
+ "TC002", # TYPE_CHECKING blocks in python3.8 (external)
213
+ "TC003", # TYPE_CHECKING blocks in python3.8
214
+ ]
215
+
216
+ [tool.ruff.lint.per-file-ignores]
217
+ 'test_*.py' = ['D']
218
+ '*_test.py' = ['D']
219
+ # The Unicode punctuation-equivalence table *is* the point -- every "ambiguous" character is a
220
+ # deliberately-listed lookalike, ported from dirac's canonicalize() (instruments-02).
221
+ 'tact/reconcile.py' = ['RUF001']
222
+ 'tests/test_tact_reconcile.py' = ['RUF001']
223
+ 'tests/test_tact_properties.py' = ['RUF001']
224
+ 'tests/test_tact_selfhost.py' = ['RUF001']
225
+
226
+ [tool.ruff.lint.flake8-pytest-style]
227
+ parametrize-names-type = "csv"
228
+
229
+ [tool.ruff.lint.flake8-comprehensions]
230
+ allow-dict-calls-with-keyword-arguments = true
231
+
232
+ [tool.ruff.lint.pydocstyle]
233
+ convention = "google"
@@ -0,0 +1,3 @@
1
+ # `tact` Python Package
2
+ This document is a high-level overview of the engineering decisions behind the tact Python package.
3
+ For general project information, see [the root directory](/README.md).
@@ -0,0 +1,133 @@
1
+ """`tact` (Latin *tactus*, touch) — a self-healing write core for agent file edits.
2
+
3
+ Agents that read a file, decide on an edit, and write it back are routinely defeated by
4
+ turbulence that has nothing to do with the edit's substance: trailing-whitespace drift,
5
+ curly-quote/dash/NBSP substitutions introduced by a copy-paste, an indentation shift from a
6
+ block moving a level deeper, or a stale line number after a concurrent edit landed first.
7
+ Builtin exact-match editors hard-fail on all of these and hand the turbulence back to the
8
+ model as a diff to re-derive — expensive, and it happens on every file, every day.
9
+
10
+ `tact` handles this in library, not in every agent's context: `reconcile` finds a target
11
+ block under a fixed, self-reporting ladder of increasing tolerance (exact -> canonicalized ->
12
+ whitespace-insensitive -> indentation-insensitive -> bounded fuzzy match), `apply` turns a
13
+ batch of such finds into one atomic, overlap-checked write, and `telemetry` counts which rung
14
+ actually did the work so a later decision about adding a neural rung is made from data.
15
+
16
+ Phase 1 shipped the reconciliation core: `reconcile.py`, `apply.py`, `telemetry.py`. Phase 2
17
+ added the read side (`read.py`, `skeleton.py`) and a persistent, stdlib-only pyrefly/ruff LSP
18
+ client (`lsp.py`), wired into `apply_edits` via an optional `lsp_manager` for post-write
19
+ diagnostics deltas. The `tact-02` feature wave rounds out the verb set in the same minimalist
20
+ mold, in two halves. Wave A: `check_edits` (dry-run preview, in `apply.py`), `patch.py`
21
+ (unified-diff / fenced-block ingestion into `Edit` batches), `transact.py` (`apply_many`,
22
+ resolve-everything-then-write-all-or-nothing across files), and `heal.py` (the
23
+ builtin-Edit-failure fixer). Wave B: `undo.py` (a content-addressed pre-image journal, wired
24
+ into every write path, with a redoable `undo` verb), `rename.py` (`textDocument/rename` via
25
+ `LspManager.rename`, converted to a multi-file `apply_many` transaction — the one verb in this
26
+ package that is *not* LSP-optional, since renaming has no honest symbolic fallback),
27
+ `anchor.py` (durable, re-findable block handles surviving a session/compaction boundary), and
28
+ `delta.py` (`read_since`, cheap re-reads that compose with `undo.py`'s journal for a real line
29
+ diff). Later phases (`instruments-03`/`-04` in the task backlog) add a console script and an
30
+ MCP skin — `tact` itself stays the one library both front ends call into (N-32/N-35: policy in
31
+ the skins, mechanism in the library).
32
+
33
+ **Provenance.** The reconciliation ladder in `reconcile.py` ports, with attribution, parts of
34
+ `dirac` (`github.com/dirac-run/dirac`, Apache-2.0, version 0.4.11) — see the `NOTICE` file at
35
+ the repo root and the provenance block in `reconcile.py`'s own module docstring for exactly
36
+ what was ported and how it was adapted.
37
+ """
38
+
39
+ from tact.anchor import (
40
+ Anchor,
41
+ AnchorCreateResult,
42
+ AnchorLoadResult,
43
+ AnchorResult,
44
+ AnchorSaveResult,
45
+ anchor,
46
+ list_anchors,
47
+ load_anchor,
48
+ resolve_anchor,
49
+ save_anchor,
50
+ )
51
+ from tact.apply import ApplyReport, CheckReport, Edit, EditResult, apply_edits, check_edits
52
+ from tact.delta import DeltaResult, DeltaStatus, read_since
53
+ from tact.heal import HealResult, heal
54
+ from tact.lsp import (
55
+ Diagnostic,
56
+ Location,
57
+ LspManager,
58
+ LspResult,
59
+ Position,
60
+ SymbolInfo,
61
+ find_project_root,
62
+ )
63
+ from tact.patch import (
64
+ PatchFileResult,
65
+ PatchParseResult,
66
+ parse_fenced_blocks,
67
+ parse_patch,
68
+ parse_unified_diff,
69
+ )
70
+ from tact.read import ReadResult, SeenHashes, read_file
71
+ from tact.reconcile import Candidate, MatchResult, Rung, canonicalize, find_block
72
+ from tact.rename import RenameResult, rename_symbol
73
+ from tact.skeleton import SkeletonEntry, SkeletonResult, skeleton
74
+ from tact.transact import ManyFileResult, ManyReport, apply_many
75
+ from tact.undo import HistoryEntry, UndoResult, history, journal_preimage, load_blob, undo
76
+
77
+ __all__ = [
78
+ 'Anchor',
79
+ 'AnchorCreateResult',
80
+ 'AnchorLoadResult',
81
+ 'AnchorResult',
82
+ 'AnchorSaveResult',
83
+ 'ApplyReport',
84
+ 'Candidate',
85
+ 'CheckReport',
86
+ 'DeltaResult',
87
+ 'DeltaStatus',
88
+ 'Diagnostic',
89
+ 'Edit',
90
+ 'EditResult',
91
+ 'HealResult',
92
+ 'HistoryEntry',
93
+ 'Location',
94
+ 'LspManager',
95
+ 'LspResult',
96
+ 'ManyFileResult',
97
+ 'ManyReport',
98
+ 'MatchResult',
99
+ 'PatchFileResult',
100
+ 'PatchParseResult',
101
+ 'Position',
102
+ 'ReadResult',
103
+ 'RenameResult',
104
+ 'Rung',
105
+ 'SeenHashes',
106
+ 'SkeletonEntry',
107
+ 'SkeletonResult',
108
+ 'SymbolInfo',
109
+ 'UndoResult',
110
+ 'anchor',
111
+ 'apply_edits',
112
+ 'apply_many',
113
+ 'canonicalize',
114
+ 'check_edits',
115
+ 'find_block',
116
+ 'find_project_root',
117
+ 'heal',
118
+ 'history',
119
+ 'journal_preimage',
120
+ 'list_anchors',
121
+ 'load_anchor',
122
+ 'load_blob',
123
+ 'parse_fenced_blocks',
124
+ 'parse_patch',
125
+ 'parse_unified_diff',
126
+ 'read_file',
127
+ 'read_since',
128
+ 'rename_symbol',
129
+ 'resolve_anchor',
130
+ 'save_anchor',
131
+ 'skeleton',
132
+ 'undo',
133
+ ]