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 +154 -0
- intact-0.7.1/README.md +136 -0
- intact-0.7.1/pyproject.toml +233 -0
- intact-0.7.1/tact/README.md +3 -0
- intact-0.7.1/tact/__init__.py +133 -0
- intact-0.7.1/tact/_home.py +35 -0
- intact-0.7.1/tact/_io.py +89 -0
- intact-0.7.1/tact/_serialize.py +54 -0
- intact-0.7.1/tact/anchor.py +331 -0
- intact-0.7.1/tact/apply.py +571 -0
- intact-0.7.1/tact/cli.py +615 -0
- intact-0.7.1/tact/delta.py +157 -0
- intact-0.7.1/tact/eval.py +393 -0
- intact-0.7.1/tact/heal.py +210 -0
- intact-0.7.1/tact/lsp.py +932 -0
- intact-0.7.1/tact/mcp_server.py +334 -0
- intact-0.7.1/tact/patch.py +376 -0
- intact-0.7.1/tact/read.py +216 -0
- intact-0.7.1/tact/reconcile.py +424 -0
- intact-0.7.1/tact/rename.py +345 -0
- intact-0.7.1/tact/skeleton.py +122 -0
- intact-0.7.1/tact/telemetry.py +70 -0
- intact-0.7.1/tact/transact.py +322 -0
- intact-0.7.1/tact/undo.py +304 -0
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
|
+
  [](/LICENSE) [](https://intact.readthedocs.io)  [](https://pypi.org/project/intact) [](https://pypi.org/project/intact)
|
|
22
|
+
|
|
23
|
+
[](https://github.com/pre-commit/pre-commit) [](https://gitlab.com/doering-ai/corpus/-/blob/main/policies/versioning.md) [](https://github.com/facebook/pyrefly) [](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
|
+
  [](/LICENSE) [](https://intact.readthedocs.io)  [](https://pypi.org/project/intact) [](https://pypi.org/project/intact)
|
|
4
|
+
|
|
5
|
+
[](https://github.com/pre-commit/pre-commit) [](https://gitlab.com/doering-ai/corpus/-/blob/main/policies/versioning.md) [](https://github.com/facebook/pyrefly) [](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,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
|
+
]
|