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.
- compact_audit/__init__.py +2 -0
- compact_audit/__main__.py +3 -0
- compact_audit/audit.py +33 -0
- compact_audit/cli.py +36 -0
- compact_audit/extract.py +88 -0
- compact_audit-0.1.0.dist-info/LICENSE +21 -0
- compact_audit-0.1.0.dist-info/METADATA +140 -0
- compact_audit-0.1.0.dist-info/RECORD +11 -0
- compact_audit-0.1.0.dist-info/WHEEL +5 -0
- compact_audit-0.1.0.dist-info/entry_points.txt +2 -0
- compact_audit-0.1.0.dist-info/top_level.txt +1 -0
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']
|
compact_audit/extract.py
ADDED
|
@@ -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 @@
|
|
|
1
|
+
compact_audit
|