compact-audit 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,2 @@
1
+ """Offline, deterministic compaction summary auditing."""
2
+ __version__ = '0.1.0'
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
compact_audit/audit.py ADDED
@@ -0,0 +1,33 @@
1
+ """Audit literal normalized surfaces, never semantic equivalence."""
2
+ from dataclasses import asdict
3
+ import re
4
+ from .extract import CATEGORIES, normalize
5
+
6
+ LIMITATION = 'Surface matching only. DROPPED means not found by surface matching — verify manually. Paraphrase is not detected; severity is a triage signal, not a verdict about meaning.'
7
+
8
+
9
+ def audit(extraction, summary, min_severity='high'):
10
+ if min_severity not in ('high', 'medium'):
11
+ raise ValueError('min_severity must be high or medium')
12
+ normalized = normalize(summary)
13
+ error_summary = normalize(re.sub(r'\d+', '', summary))
14
+ totals = {category: {'kept': 0, 'dropped': 0} for category in CATEGORIES}
15
+ dropped = []
16
+ for item in extraction.items:
17
+ target = error_summary if item.category == 'error' else normalized
18
+ kept = bool(item.key and item.key in target)
19
+ if item.category == 'file_edit':
20
+ kept = kept or bool(item.basename and item.basename in target)
21
+ totals[item.category]['kept' if kept else 'dropped'] += 1
22
+ if not kept:
23
+ dropped.append(asdict(item))
24
+ severe = any(item['severity'] == 'HIGH' or min_severity == 'medium' for item in dropped)
25
+ exit_code = 2 if extraction.parse_errors or severe else (1 if dropped else 0)
26
+ return {
27
+ 'totals': totals, 'total_items': len(extraction.items),
28
+ 'kept': len(extraction.items) - len(dropped), 'dropped_count': len(dropped),
29
+ 'dropped': dropped, 'parse_errors': extraction.parse_errors,
30
+ 'truncated': extraction.truncated, 'min_severity': min_severity,
31
+ 'verdict': {0: 'PASS', 1: 'WARN', 2: 'FAIL'}[exit_code],
32
+ 'exit_code': exit_code, 'limitation': LIMITATION,
33
+ }
compact_audit/cli.py ADDED
@@ -0,0 +1,36 @@
1
+ """Command-line interface."""
2
+ import argparse
3
+ import json
4
+ from pathlib import Path
5
+ from . import __version__
6
+ from .audit import audit
7
+ from .extract import extract, read_transcript
8
+
9
+
10
+ def main(argv=None):
11
+ parser = argparse.ArgumentParser(prog='compact-audit', description='Offline surface audit of compaction context loss.')
12
+ parser.add_argument('--version', action='version', version='%(prog)s ' + __version__)
13
+ commands = parser.add_subparsers(dest='command', required=True)
14
+ command = commands.add_parser('audit', help='compare a transcript with its summary')
15
+ command.add_argument('transcript', type=Path)
16
+ command.add_argument('summary', type=Path)
17
+ command.add_argument('--json', action='store_true', dest='json_output')
18
+ command.add_argument('--min-severity', choices=('high', 'medium'), default='high')
19
+ args = parser.parse_args(argv)
20
+ try:
21
+ lines, errors = read_transcript(args.transcript)
22
+ report = audit(extract(lines, errors), args.summary.read_text(encoding='utf-8'), args.min_severity)
23
+ except (OSError, UnicodeError) as exc:
24
+ parser.error(str(exc))
25
+ if args.json_output:
26
+ print(json.dumps(report, ensure_ascii=False, indent=2))
27
+ else:
28
+ print('{verdict}: {kept} kept, {dropped_count} dropped / {total_items} items'.format(**report))
29
+ for category, counts in report['totals'].items():
30
+ print('{}: {} kept, {} dropped'.format(category, counts['kept'], counts['dropped']))
31
+ print('Parse errors: {}'.format(report['parse_errors']))
32
+ print('Truncated: {} (maximum 500 items)'.format(report['truncated']))
33
+ for item in report['dropped']:
34
+ print('DROPPED [{severity}] {category}: {key}\n {text}'.format(**item))
35
+ print(report['limitation'])
36
+ return report['exit_code']
@@ -0,0 +1,88 @@
1
+ """Deterministic, deliberately conservative surface-pattern extraction."""
2
+ import json
3
+ import re
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+
7
+ CATEGORIES = ('decision', 'constraint', 'error', 'file_edit', 'todo')
8
+ MAX_ITEMS = 500
9
+ SOURCE_LIMIT = 500
10
+
11
+
12
+ def normalize(text):
13
+ return re.sub(r'[\W_]+', ' ', text.lower()).strip()
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class Item:
18
+ category: str
19
+ key: str
20
+ text: str
21
+ severity: str
22
+ basename: str = ''
23
+
24
+
25
+ @dataclass
26
+ class Extraction:
27
+ items: list
28
+ parse_errors: int = 0
29
+ truncated: bool = False
30
+
31
+
32
+ DECISION = re.compile(r'\b(?:decision\s*:|decided to\b|we will use\b|chosen\b|agreed to\b|going with\b)', re.I)
33
+ CONSTRAINT = re.compile(r'\b(?:must not\b|never\s+\S|do not\b|must always\b|only\s+\S|required\b)', re.I)
34
+ ERROR = re.compile(r'\b(?:error\b|traceback\b|failed\b|failed with\b|exit code\s*[:=]?\s*-?0*[1-9]\d*\b)', re.I)
35
+ EDIT = re.compile(r'\b(?:edited|changed|created|deleted|modified|wrote|rewrote|updated)\b', re.I)
36
+ PATH = re.compile(r'(?<![\w/.-])(?:[\w.-]+/)*[\w.-]+\.(?:py|js|ts|md|json|yaml|toml|go|rs)\b|(?<![\w/.-])(?:[\w.-]+/)*(?:src|tests|docs)/[\w./-]+', re.I)
37
+ TODO = re.compile(r'\b(?:todo|fixme|not done|remaining|next step|still need)\b', re.I)
38
+
39
+
40
+ def read_transcript(path):
41
+ """Return source lines and errors; JSONL must contain objects with text."""
42
+ path = Path(path)
43
+ lines, errors = [], 0
44
+ for raw in path.read_text(encoding='utf-8').splitlines():
45
+ if not raw.strip():
46
+ continue
47
+ if path.suffix.lower() == '.jsonl':
48
+ try:
49
+ obj = json.loads(raw)
50
+ if not isinstance(obj, dict):
51
+ raise ValueError('expected object')
52
+ value = next((obj[k] for k in ('content', 'text', 'message') if k in obj), '')
53
+ if not isinstance(value, str):
54
+ raise ValueError('expected text')
55
+ except (ValueError, TypeError):
56
+ errors += 1
57
+ continue
58
+ lines.extend(line for line in value.splitlines() if line.strip())
59
+ else:
60
+ lines.append(raw)
61
+ return lines, errors
62
+
63
+
64
+ def extract(lines, parse_errors=0):
65
+ items, seen = [], set()
66
+ truncated = False
67
+ for line in lines:
68
+ candidates = []
69
+ for category, pattern in (('decision', DECISION), ('constraint', CONSTRAINT), ('error', ERROR)):
70
+ if pattern.search(line):
71
+ key = normalize(re.sub(r'\d+', '', line) if category == 'error' else line)
72
+ candidates.append(Item(category, key, line[:SOURCE_LIMIT], 'HIGH'))
73
+ if EDIT.search(line):
74
+ for match in PATH.finditer(line):
75
+ path = match.group().rstrip('./')
76
+ candidates.append(Item('file_edit', normalize(path), line[:SOURCE_LIMIT], 'MEDIUM', normalize(path.rsplit('/', 1)[-1])))
77
+ if TODO.search(line):
78
+ candidates.append(Item('todo', normalize(line), line[:SOURCE_LIMIT], 'MEDIUM'))
79
+ for item in candidates:
80
+ identity = (item.category, item.key)
81
+ if identity in seen:
82
+ continue
83
+ seen.add(identity)
84
+ if len(items) < MAX_ITEMS:
85
+ items.append(item)
86
+ else:
87
+ truncated = True
88
+ return Extraction(items, parse_errors, truncated)
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 compact-audit contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,140 @@
1
+ Metadata-Version: 2.1
2
+ Name: compact-audit
3
+ Version: 0.1.0
4
+ Summary: Offline surface matching audit of compaction summaries
5
+ License: MIT License
6
+
7
+ Copyright (c) 2026 compact-audit contributors
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
26
+
27
+ Requires-Python: >=3.9
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+
31
+ # compact-audit
32
+
33
+ An offline, deterministic Python CLI that compares a pre-compaction transcript
34
+ with the summary or handover note that replaced it. It extracts critical items
35
+ and reports which surfaces were kept or dropped, with category totals and
36
+ severity for triage. No LLM, network calls, keys, or runtime dependencies.
37
+ Python 3.9+; MIT licensed.
38
+
39
+ ## Usage
40
+
41
+ Install the locally built wheel with `python -m pip install --no-index dist/compact_audit-0.1.0-py3-none-any.whl`.
42
+
43
+ ```sh
44
+ compact-audit --version
45
+ compact-audit audit examples/sample-transcript.jsonl examples/sample-summary.md
46
+ compact-audit audit examples/sample-transcript.jsonl examples/sample-summary.md --json
47
+ compact-audit audit transcript.txt summary.md --min-severity medium
48
+ python -m compact_audit --help
49
+ ```
50
+
51
+ The included examples are entirely synthetic. Their full-coverage summary
52
+ returns PASS (exit 0). Removing its migration decision makes the audit FAIL
53
+ (exit 2). JSON reports contain the same totals, dropped items, parse-error count,
54
+ truncation indicator, verdict, and limitation as human reports.
55
+
56
+ ## Inputs and extraction
57
+
58
+ Transcripts are UTF-8 JSONL (`.jsonl`, case insensitive) or plain text (`.txt` or
59
+ `.md`). Plain text is split into non-empty lines. JSONL requires one object per
60
+ non-empty line; fields use `content`, then `text`, then `message` when the earlier
61
+ field is absent. Other fields are ignored. Missing text fields yield no items;
62
+ non-string selected fields, non-objects, and malformed JSON count as parse
63
+ errors. Multiline strings are split into source lines. Blank lines are ignored.
64
+ The summary is a UTF-8 text or Markdown file. Empty transcripts yield zero items;
65
+ empty summaries drop every extracted item.
66
+
67
+ These short, case-insensitive pattern lists are heuristic:
68
+
69
+ | Category | Triggers | Severity if dropped |
70
+ | --- | --- | --- |
71
+ | decision | `Decision:`, `decided to`, `we will use`, `chosen`, `agreed to`, `going with` | HIGH |
72
+ | constraint | `must not`, `never `, `do not`, `must always`, `only ` followed by a non-empty clause, `required` | HIGH |
73
+ | error | word `Error`, `Traceback`, `FAILED`, `failed with`, or `exit code` followed by a non-zero integer | HIGH |
74
+ | file_edit | `edited`, `changed`, `created`, `deleted`, `modified`, `wrote`, `rewrote`, `updated` plus a recognized path | MEDIUM |
75
+ | todo | `TODO`, `FIXME`, `not done`, `remaining`, `next step`, `still need` | MEDIUM |
76
+
77
+ Recognized file paths end in `.py`, `.js`, `.ts`, `.md`, `.json`, `.yaml`,
78
+ `.toml`, `.go`, or `.rs`, or are under `src/`, `tests/`, or `docs/`.
79
+ Each matched path becomes a separate item. A line can produce several categories.
80
+ Items deduplicate by category and normalized key. The first 500 unique items in
81
+ transcript order are retained; additional unique items set `truncated` to true.
82
+ Within a line, category order is decision, constraint, error, file_edit, todo.
83
+ Source text is truncated to 500 characters; matching keys retain the full line.
84
+
85
+ Matching lowercases text and collapses punctuation, underscores, and whitespace
86
+ to single spaces. Whole normalized line keys must occur as substrings of the
87
+ normalized summary. Error keys strip digits; the summary also strips digits for
88
+ error matching. File edits match either their normalized full path or basename.
89
+
90
+ ## Exit codes
91
+
92
+ | Code | Verdict | Meaning |
93
+ | --- | --- | --- |
94
+ | 0 | PASS | No dropped items |
95
+ | 1 | WARN | Dropped items exist, but none reach the selected threshold |
96
+ | 2 | FAIL | A dropped item reaches the threshold, or any parse error occurred |
97
+
98
+ The default threshold is `high`: MEDIUM-only drops warn with exit 1. With
99
+ `--min-severity medium`, any drop fails with exit 2. Thus an audit with no
100
+ threshold-level drops can still warn; PASS is reserved for no drops. Input I/O,
101
+ encoding, and CLI usage errors also return 2 with an explanatory diagnostic.
102
+
103
+ ## Honest limitations
104
+
105
+ This is surface matching, not semantic judgement. Paraphrase is not detected
106
+ and causes false DROPPED results. DROPPED means **not found by surface matching —
107
+ verify manually**, rather than certainty that meaning was lost. Severity and
108
+ thresholds are triage signals, not evidence that a summary is safe or complete.
109
+
110
+ Heuristic patterns can miss implicit decisions or overmatch incidental language
111
+ (such as unrestricted uses of “only”). Basename matches may conflate different
112
+ files; substring matches can accept negated statements. Digit stripping can
113
+ conflate distinct errors. A PASS covers only extracted surfaces, and truncation
114
+ leaves later items unaudited. Review truncation and the original context manually.
115
+ The tool does not inspect repositories, execute transcript content, validate
116
+ claims, or recover missing context.
117
+
118
+ ## Differentiation
119
+
120
+ - **Succession** is a Claude-Code-only plugin with git-pointer claim checks;
121
+ compact-audit is an offline cross-harness CLI over transcript and summary text.
122
+ - **session-handover** generates a handover pack; compact-audit audits any summary,
123
+ including auto-compaction output, for loss.
124
+ - **catnap** prevents mid-task compaction; compact-audit measures surface items a
125
+ completed compaction left out.
126
+ - **model-canary** probes model drift; compact-audit audits context loss.
127
+
128
+ ## Development
129
+
130
+ Runtime and tests use only the Python standard library. Packaging uses setuptools
131
+ and wheel through `pyproject.toml`, with the standard `build` frontend.
132
+
133
+ ```sh
134
+ PYTHONPATH=src python -m unittest discover -s tests -v
135
+ python -m build
136
+ ```
137
+
138
+ For an offline environment with build, setuptools, and wheel already installed,
139
+ use `python -m build --no-isolation` to avoid dependency downloads. Build outputs
140
+ are artifacts under `dist/`, not source files.
@@ -0,0 +1,11 @@
1
+ compact_audit/__init__.py,sha256=315WeNzsOHfv_sRYTZH7pnFyUyp1PyLNkLnZdtQvrQo,80
2
+ compact_audit/__main__.py,sha256=k1ocEWawweo1qCJWNFAAvyxz3tcY13dzvCenHszij30,48
3
+ compact_audit/audit.py,sha256=bu5hB1kCZtLF90SA9qVfXXppZYIV9WO625Q04X-iVgs,1713
4
+ compact_audit/cli.py,sha256=MIZjZvvs_SxWhqEGpaPdBTUoOabWzKgoeOa_jg5gmAE,1810
5
+ compact_audit/extract.py,sha256=WYNo2ar7XtI0qBwo7VOdztb_qF5sMkbnZheKvn8YoHE,3333
6
+ compact_audit-0.1.0.dist-info/LICENSE,sha256=6FF-SpUjQEStFXE1nIYXbJ7ZFJex4M3T8xrJygLVAEM,1083
7
+ compact_audit-0.1.0.dist-info/METADATA,sha256=mgIvlTIp7Da2_9uAUZ7O8U--7tlOHOCrP-oBMj7D9yA,7104
8
+ compact_audit-0.1.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
9
+ compact_audit-0.1.0.dist-info/entry_points.txt,sha256=SG58MUyofG2r_pMyZNe-26y-28xi_NhtGo_eNvjX6Ts,57
10
+ compact_audit-0.1.0.dist-info/top_level.txt,sha256=nr9HvYv80YGr9CPeWKKzh1R9frSKamMuUgyBwCQUzcI,14
11
+ compact_audit-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.42.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ compact-audit = compact_audit.cli:main
@@ -0,0 +1 @@
1
+ compact_audit