assurance-cli 0.2.3__tar.gz → 0.3.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.
- {assurance_cli-0.2.3 → assurance_cli-0.3.1}/PKG-INFO +10 -2
- {assurance_cli-0.2.3 → assurance_cli-0.3.1}/README.md +8 -0
- {assurance_cli-0.2.3 → assurance_cli-0.3.1}/assurance_cli/gather.py +18 -5
- {assurance_cli-0.2.3 → assurance_cli-0.3.1}/assurance_cli/setdiff.py +27 -6
- {assurance_cli-0.2.3 → assurance_cli-0.3.1}/assurance_cli.egg-info/PKG-INFO +10 -2
- {assurance_cli-0.2.3 → assurance_cli-0.3.1}/assurance_cli.egg-info/requires.txt +1 -1
- {assurance_cli-0.2.3 → assurance_cli-0.3.1}/pyproject.toml +2 -2
- {assurance_cli-0.2.3 → assurance_cli-0.3.1}/tests/test_cli.py +23 -0
- {assurance_cli-0.2.3 → assurance_cli-0.3.1}/tests/test_setdiff.py +46 -1
- {assurance_cli-0.2.3 → assurance_cli-0.3.1}/LICENSE +0 -0
- {assurance_cli-0.2.3 → assurance_cli-0.3.1}/assurance_cli/__init__.py +0 -0
- {assurance_cli-0.2.3 → assurance_cli-0.3.1}/assurance_cli/__main__.py +0 -0
- {assurance_cli-0.2.3 → assurance_cli-0.3.1}/assurance_cli/baseline.py +0 -0
- {assurance_cli-0.2.3 → assurance_cli-0.3.1}/assurance_cli/cli.py +0 -0
- {assurance_cli-0.2.3 → assurance_cli-0.3.1}/assurance_cli/paths.py +0 -0
- {assurance_cli-0.2.3 → assurance_cli-0.3.1}/assurance_cli/profile.py +0 -0
- {assurance_cli-0.2.3 → assurance_cli-0.3.1}/assurance_cli.egg-info/SOURCES.txt +0 -0
- {assurance_cli-0.2.3 → assurance_cli-0.3.1}/assurance_cli.egg-info/dependency_links.txt +0 -0
- {assurance_cli-0.2.3 → assurance_cli-0.3.1}/assurance_cli.egg-info/entry_points.txt +0 -0
- {assurance_cli-0.2.3 → assurance_cli-0.3.1}/assurance_cli.egg-info/top_level.txt +0 -0
- {assurance_cli-0.2.3 → assurance_cli-0.3.1}/setup.cfg +0 -0
- {assurance_cli-0.2.3 → assurance_cli-0.3.1}/tests/test_read_only.py +0 -0
- {assurance_cli-0.2.3 → assurance_cli-0.3.1}/tests/test_version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: assurance-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: CLI for folder assurance checks — coverage, staleness, and baselines.
|
|
5
5
|
Author-email: "I-Ops Operations Intelligence, LLC" <hello@i-ops.dev>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -19,7 +19,7 @@ Classifier: Topic :: Software Development :: Quality Assurance
|
|
|
19
19
|
Requires-Python: >=3.10
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
License-File: LICENSE
|
|
22
|
-
Requires-Dist: assurance-core>=0.
|
|
22
|
+
Requires-Dist: assurance-core>=0.7
|
|
23
23
|
Requires-Dist: openpyxl>=3.1
|
|
24
24
|
Provides-Extra: dev
|
|
25
25
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
@@ -99,6 +99,14 @@ assurance check ~/invoices --expect numbered
|
|
|
99
99
|
Monthly, quarterly, weekly, daily, numbered. That last line is the **derivation**: it prints with
|
|
100
100
|
every ratio so you can disagree with the denominator, not just the result.
|
|
101
101
|
|
|
102
|
+
When a file is there under a name it can't read, it says so beside the gap, because that's the
|
|
103
|
+
difference between *never produced* and *produced and named differently*:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
11 of 12 months from 2025-01 to 2025-12 — not in this folder: March 2025
|
|
107
|
+
— 1 name here could not be read as any of them: March FINAL v2.csv
|
|
108
|
+
```
|
|
109
|
+
|
|
102
110
|
### `assurance init` — did anything change underneath?
|
|
103
111
|
|
|
104
112
|
```bash
|
|
@@ -72,6 +72,14 @@ assurance check ~/invoices --expect numbered
|
|
|
72
72
|
Monthly, quarterly, weekly, daily, numbered. That last line is the **derivation**: it prints with
|
|
73
73
|
every ratio so you can disagree with the denominator, not just the result.
|
|
74
74
|
|
|
75
|
+
When a file is there under a name it can't read, it says so beside the gap, because that's the
|
|
76
|
+
difference between *never produced* and *produced and named differently*:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
11 of 12 months from 2025-01 to 2025-12 — not in this folder: March 2025
|
|
80
|
+
— 1 name here could not be read as any of them: March FINAL v2.csv
|
|
81
|
+
```
|
|
82
|
+
|
|
75
83
|
### `assurance init` — did anything change underneath?
|
|
76
84
|
|
|
77
85
|
```bash
|
|
@@ -25,6 +25,8 @@ from assurance_cli.profile import TABULAR_SUFFIXES, profile_file
|
|
|
25
25
|
|
|
26
26
|
MAX_PERIODS = 36
|
|
27
27
|
MAX_NUMBERED = 500
|
|
28
|
+
# Enough to say "it is probably right there" without pasting a whole directory into one sentence.
|
|
29
|
+
MAX_UNREAD = 20
|
|
28
30
|
MIN_FILES_TO_INFER = 3
|
|
29
31
|
|
|
30
32
|
_KIND_FROM_NAME = {
|
|
@@ -39,7 +41,7 @@ _KIND_FROM_NAME = {
|
|
|
39
41
|
def list_dated_files(folder: str) -> dict[str, Any]:
|
|
40
42
|
"""List sequence points present in a folder from dated tabular filenames."""
|
|
41
43
|
root = resolve_folder(folder)
|
|
42
|
-
by_key = _indexed_files(root)
|
|
44
|
+
by_key, unread = _indexed_files(root)
|
|
43
45
|
keys = sorted(by_key)
|
|
44
46
|
return {
|
|
45
47
|
"folder": str(root),
|
|
@@ -65,7 +67,7 @@ def check_coverage(
|
|
|
65
67
|
) -> dict[str, Any]:
|
|
66
68
|
"""Check whether every step in the span is present — cold start, no prior state."""
|
|
67
69
|
root = resolve_folder(folder)
|
|
68
|
-
by_key = _indexed_files(root)
|
|
70
|
+
by_key, unread = _indexed_files(root)
|
|
69
71
|
if not by_key:
|
|
70
72
|
return {
|
|
71
73
|
"folder": str(root),
|
|
@@ -132,7 +134,7 @@ def check_coverage(
|
|
|
132
134
|
truncated = f"stopped at {cap} {unit}"
|
|
133
135
|
expected_keys = expected_keys[-cap:]
|
|
134
136
|
|
|
135
|
-
cov = Coverage(scope_label=scope, truncated=truncated, derivation=derivation)
|
|
137
|
+
cov = Coverage(scope_label=scope, truncated=truncated, derivation=derivation, unmatched=unread)
|
|
136
138
|
for key, label in expected_keys:
|
|
137
139
|
expectation = Expectation(key=key, label=label, why=derivation or f"in {scope}")
|
|
138
140
|
cov.expected.append(expectation)
|
|
@@ -218,8 +220,17 @@ def check_staleness(
|
|
|
218
220
|
return _finding_to_dict(finding)
|
|
219
221
|
|
|
220
222
|
|
|
221
|
-
def _indexed_files(root: Path) -> dict[str, list[Path]]:
|
|
223
|
+
def _indexed_files(root: Path) -> tuple[dict[str, list[Path]], list[str]]:
|
|
224
|
+
"""The files that parse to a point, and the names of the tabular files that do not.
|
|
225
|
+
|
|
226
|
+
That second list used to be `continue` and nothing else. It is the difference between two causes
|
|
227
|
+
that produce an identical `missing` line — never produced, or produced under a name the
|
|
228
|
+
enumeration could not read — and it was being discarded at the one moment we had it. A folder of
|
|
229
|
+
twelve files, eleven parsing as months and one called "March FINAL v2.csv", reported March as
|
|
230
|
+
not in the folder and never mentioned the twelfth file anywhere.
|
|
231
|
+
"""
|
|
222
232
|
found: dict[str, list[Path]] = {}
|
|
233
|
+
unread: list[str] = []
|
|
223
234
|
for path in sorted(root.rglob("*")):
|
|
224
235
|
if not path.is_file() or path.suffix.lower() not in TABULAR_SUFFIXES:
|
|
225
236
|
continue
|
|
@@ -231,10 +242,11 @@ def _indexed_files(root: Path) -> dict[str, list[Path]]:
|
|
|
231
242
|
continue
|
|
232
243
|
point = point_from_filename(path.name)
|
|
233
244
|
if point is None:
|
|
245
|
+
unread.append(path.name)
|
|
234
246
|
continue
|
|
235
247
|
key = point_key(point) # type: ignore[arg-type]
|
|
236
248
|
found.setdefault(key, []).append(path)
|
|
237
|
-
return found
|
|
249
|
+
return found, unread[:MAX_UNREAD]
|
|
238
250
|
|
|
239
251
|
|
|
240
252
|
def _label_for_key(key: str, filename: str) -> str:
|
|
@@ -296,6 +308,7 @@ def _coverage_to_dict(cov: Coverage) -> dict[str, Any]:
|
|
|
296
308
|
"gone": dict(cov.gone),
|
|
297
309
|
"ambiguous": dict(cov.ambiguous),
|
|
298
310
|
"unreadable": dict(cov.unreadable),
|
|
311
|
+
"unmatched": list(cov.unmatched),
|
|
299
312
|
"unauthorized": dict(cov.unauthorized),
|
|
300
313
|
"truncated": cov.truncated,
|
|
301
314
|
"undetermined": cov.undetermined,
|
|
@@ -120,19 +120,40 @@ def _keys_from_json(payload: Any, *, label: str) -> list[str]:
|
|
|
120
120
|
if isinstance(entry, str):
|
|
121
121
|
keys.append(entry)
|
|
122
122
|
elif isinstance(entry, dict):
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
keys.append(entry[field])
|
|
126
|
-
break
|
|
127
|
-
else:
|
|
123
|
+
found = _identifier(entry)
|
|
124
|
+
if found is None:
|
|
128
125
|
raise KeySpecError(
|
|
129
|
-
f"{label}: entry {index} is an object with
|
|
126
|
+
f"{label}: entry {index} is an object with none of {', '.join(_ID_FIELDS)}, "
|
|
127
|
+
"at the top level or under 'metadata'"
|
|
130
128
|
)
|
|
129
|
+
keys.append(found)
|
|
131
130
|
else:
|
|
132
131
|
raise KeySpecError(f"{label}: entry {index} is {type(entry).__name__}, not a key")
|
|
133
132
|
return keys
|
|
134
133
|
|
|
135
134
|
|
|
135
|
+
# What a chunk record from a vector store calls its parent document. `document`/`doc`/`source`
|
|
136
|
+
# were added in 0.3.1 so a retrieval payload works without being reshaped first: chunks are what a
|
|
137
|
+
# retriever returns, documents are what a scope is declared in, and the de-duplication below turns
|
|
138
|
+
# five chunks of one document into one document covered. Diffing chunks directly is the mistake that
|
|
139
|
+
# makes a coverage record report a gap nobody can ever close.
|
|
140
|
+
_ID_FIELDS = ("key", "id", "name", "path", "document", "doc", "source", "document_id", "doc_id")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _identifier(entry: dict[str, Any]) -> str | None:
|
|
144
|
+
for field in _ID_FIELDS:
|
|
145
|
+
value = entry.get(field)
|
|
146
|
+
if isinstance(value, str) and value:
|
|
147
|
+
return value
|
|
148
|
+
metadata = entry.get("metadata")
|
|
149
|
+
if isinstance(metadata, dict):
|
|
150
|
+
for field in _ID_FIELDS:
|
|
151
|
+
value = metadata.get(field)
|
|
152
|
+
if isinstance(value, str) and value:
|
|
153
|
+
return value
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
|
|
136
157
|
def _dedupe(values: Any) -> list[str]:
|
|
137
158
|
seen: set[str] = set()
|
|
138
159
|
out: list[str] = []
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: assurance-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: CLI for folder assurance checks — coverage, staleness, and baselines.
|
|
5
5
|
Author-email: "I-Ops Operations Intelligence, LLC" <hello@i-ops.dev>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -19,7 +19,7 @@ Classifier: Topic :: Software Development :: Quality Assurance
|
|
|
19
19
|
Requires-Python: >=3.10
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
License-File: LICENSE
|
|
22
|
-
Requires-Dist: assurance-core>=0.
|
|
22
|
+
Requires-Dist: assurance-core>=0.7
|
|
23
23
|
Requires-Dist: openpyxl>=3.1
|
|
24
24
|
Provides-Extra: dev
|
|
25
25
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
@@ -99,6 +99,14 @@ assurance check ~/invoices --expect numbered
|
|
|
99
99
|
Monthly, quarterly, weekly, daily, numbered. That last line is the **derivation**: it prints with
|
|
100
100
|
every ratio so you can disagree with the denominator, not just the result.
|
|
101
101
|
|
|
102
|
+
When a file is there under a name it can't read, it says so beside the gap, because that's the
|
|
103
|
+
difference between *never produced* and *produced and named differently*:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
11 of 12 months from 2025-01 to 2025-12 — not in this folder: March 2025
|
|
107
|
+
— 1 name here could not be read as any of them: March FINAL v2.csv
|
|
108
|
+
```
|
|
109
|
+
|
|
102
110
|
### `assurance init` — did anything change underneath?
|
|
103
111
|
|
|
104
112
|
```bash
|
|
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "assurance-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.1"
|
|
8
8
|
description = "CLI for folder assurance checks — coverage, staleness, and baselines."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "Apache-2.0" }
|
|
11
11
|
authors = [{ name = "I-Ops Operations Intelligence, LLC", email = "hello@i-ops.dev" }]
|
|
12
12
|
requires-python = ">=3.10"
|
|
13
13
|
dependencies = [
|
|
14
|
-
"assurance-core>=0.
|
|
14
|
+
"assurance-core>=0.7",
|
|
15
15
|
"openpyxl>=3.1",
|
|
16
16
|
]
|
|
17
17
|
keywords = ["cli", "ai-agents", "governance", "auditing", "assurance", "reproducibility"]
|
|
@@ -154,3 +154,26 @@ def test_the_nested_record_agrees_with_the_wrapper(
|
|
|
154
154
|
def test_a_folder_that_checks_out_still_exits_zero(monthly_folder: Path) -> None:
|
|
155
155
|
"""The counterweight: the ordinary path must not start failing."""
|
|
156
156
|
assert main(["check", str(monthly_folder)]) == 0
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_a_file_named_outside_the_scheme_is_reported_beside_the_gap(tmp_path: Path) -> None:
|
|
160
|
+
"""The case an outside reader described on 2026-08-29: eleven months parse, March is "not in
|
|
161
|
+
this folder", and a file called `March FINAL v2.csv` sits there unmentioned. Knowing a name here
|
|
162
|
+
could not be read is what separates "never produced" from "produced and named differently"."""
|
|
163
|
+
for month in ("01", "02", "04", "05", "06", "07", "08", "09", "10", "11", "12"):
|
|
164
|
+
(tmp_path / f"2025-{month}-report.csv").write_text("a,b\n1,2\n", encoding="utf-8")
|
|
165
|
+
(tmp_path / "March FINAL v2.csv").write_text("a,b\n1,2\n", encoding="utf-8")
|
|
166
|
+
|
|
167
|
+
result = check_coverage(str(tmp_path))
|
|
168
|
+
|
|
169
|
+
assert result["coverage"]["unmatched"] == ["March FINAL v2.csv"]
|
|
170
|
+
assert "could not be read as any of them" in result["summary"]
|
|
171
|
+
assert "March FINAL v2.csv" in result["summary"]
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_a_folder_where_everything_parses_reports_nothing_unread(monthly_folder: Path) -> None:
|
|
175
|
+
"""The clause must stay rare enough to mean something."""
|
|
176
|
+
result = check_coverage(str(monthly_folder))
|
|
177
|
+
|
|
178
|
+
assert result["coverage"]["unmatched"] == []
|
|
179
|
+
assert "could not be read" not in result["summary"]
|
|
@@ -81,7 +81,7 @@ def test_an_object_entry_with_no_identifying_field_is_refused(tmp_path: Path) ->
|
|
|
81
81
|
listing = tmp_path / "found.json"
|
|
82
82
|
listing.write_text('[{"score": 0.9}]', encoding="utf-8")
|
|
83
83
|
|
|
84
|
-
with pytest.raises(KeySpecError, match="
|
|
84
|
+
with pytest.raises(KeySpecError, match="none of"):
|
|
85
85
|
read_keys(str(listing), label="--found")
|
|
86
86
|
|
|
87
87
|
|
|
@@ -236,3 +236,48 @@ def test_a_single_line_is_never_a_table(tmp_path: Path) -> None:
|
|
|
236
236
|
listing.write_text("a,b,c\n", encoding="utf-8")
|
|
237
237
|
|
|
238
238
|
assert read_keys(str(listing), label="--expected") == ["a,b,c"]
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# --- a chunk payload from a vector store ---------------------------------------------------------
|
|
242
|
+
#
|
|
243
|
+
# Added 0.3.1 after building a real retrieval pipeline. A retriever returns chunks with the parent
|
|
244
|
+
# document under `source` or `metadata.source`; a scope is declared in documents. Requiring the
|
|
245
|
+
# caller to reshape that first is friction with no honesty benefit, and the de-duplication is what
|
|
246
|
+
# turns five chunks of one document into one document covered.
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def test_a_vector_store_payload_is_read_without_reshaping(tmp_path: Path) -> None:
|
|
250
|
+
listing = tmp_path / "chunks.json"
|
|
251
|
+
listing.write_text(json.dumps([
|
|
252
|
+
{"text": "...", "score": 0.84, "metadata": {"source": "acme/msa-2023.md"}},
|
|
253
|
+
{"text": "...", "score": 0.77, "metadata": {"source": "acme/msa-2023.md"}},
|
|
254
|
+
{"text": "...", "score": 0.75, "metadata": {"source": "acme/sla-exhibit-b.md"}},
|
|
255
|
+
]), encoding="utf-8")
|
|
256
|
+
|
|
257
|
+
assert read_keys(str(listing), label="--found") == ["acme/msa-2023.md", "acme/sla-exhibit-b.md"]
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@pytest.mark.parametrize("field", ["document", "doc", "source", "document_id", "doc_id"])
|
|
261
|
+
def test_the_common_parent_document_fields_are_read(tmp_path: Path, field: str) -> None:
|
|
262
|
+
listing = tmp_path / "chunks.json"
|
|
263
|
+
listing.write_text(json.dumps([{field: "acme/msa-2023.md", "text": "..."}]), encoding="utf-8")
|
|
264
|
+
|
|
265
|
+
assert read_keys(str(listing), label="--found") == ["acme/msa-2023.md"]
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def test_five_chunks_from_two_documents_are_two_keys(tmp_path: Path) -> None:
|
|
269
|
+
"""Five chunks of one document is one document covered, not five."""
|
|
270
|
+
listing = tmp_path / "chunks.json"
|
|
271
|
+
listing.write_text(json.dumps(
|
|
272
|
+
[{"source": "a.md"}] * 4 + [{"source": "b.md"}]), encoding="utf-8")
|
|
273
|
+
|
|
274
|
+
assert read_keys(str(listing), label="--found") == ["a.md", "b.md"]
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def test_a_chunk_with_no_identifying_field_still_refuses(tmp_path: Path) -> None:
|
|
278
|
+
"""Widening the accepted fields must not turn into guessing at somebody's schema."""
|
|
279
|
+
listing = tmp_path / "chunks.json"
|
|
280
|
+
listing.write_text(json.dumps([{"text": "...", "score": 0.9}]), encoding="utf-8")
|
|
281
|
+
|
|
282
|
+
with pytest.raises(KeySpecError, match="none of"):
|
|
283
|
+
read_keys(str(listing), label="--found")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|