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.
Files changed (23) hide show
  1. {assurance_cli-0.2.3 → assurance_cli-0.3.1}/PKG-INFO +10 -2
  2. {assurance_cli-0.2.3 → assurance_cli-0.3.1}/README.md +8 -0
  3. {assurance_cli-0.2.3 → assurance_cli-0.3.1}/assurance_cli/gather.py +18 -5
  4. {assurance_cli-0.2.3 → assurance_cli-0.3.1}/assurance_cli/setdiff.py +27 -6
  5. {assurance_cli-0.2.3 → assurance_cli-0.3.1}/assurance_cli.egg-info/PKG-INFO +10 -2
  6. {assurance_cli-0.2.3 → assurance_cli-0.3.1}/assurance_cli.egg-info/requires.txt +1 -1
  7. {assurance_cli-0.2.3 → assurance_cli-0.3.1}/pyproject.toml +2 -2
  8. {assurance_cli-0.2.3 → assurance_cli-0.3.1}/tests/test_cli.py +23 -0
  9. {assurance_cli-0.2.3 → assurance_cli-0.3.1}/tests/test_setdiff.py +46 -1
  10. {assurance_cli-0.2.3 → assurance_cli-0.3.1}/LICENSE +0 -0
  11. {assurance_cli-0.2.3 → assurance_cli-0.3.1}/assurance_cli/__init__.py +0 -0
  12. {assurance_cli-0.2.3 → assurance_cli-0.3.1}/assurance_cli/__main__.py +0 -0
  13. {assurance_cli-0.2.3 → assurance_cli-0.3.1}/assurance_cli/baseline.py +0 -0
  14. {assurance_cli-0.2.3 → assurance_cli-0.3.1}/assurance_cli/cli.py +0 -0
  15. {assurance_cli-0.2.3 → assurance_cli-0.3.1}/assurance_cli/paths.py +0 -0
  16. {assurance_cli-0.2.3 → assurance_cli-0.3.1}/assurance_cli/profile.py +0 -0
  17. {assurance_cli-0.2.3 → assurance_cli-0.3.1}/assurance_cli.egg-info/SOURCES.txt +0 -0
  18. {assurance_cli-0.2.3 → assurance_cli-0.3.1}/assurance_cli.egg-info/dependency_links.txt +0 -0
  19. {assurance_cli-0.2.3 → assurance_cli-0.3.1}/assurance_cli.egg-info/entry_points.txt +0 -0
  20. {assurance_cli-0.2.3 → assurance_cli-0.3.1}/assurance_cli.egg-info/top_level.txt +0 -0
  21. {assurance_cli-0.2.3 → assurance_cli-0.3.1}/setup.cfg +0 -0
  22. {assurance_cli-0.2.3 → assurance_cli-0.3.1}/tests/test_read_only.py +0 -0
  23. {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.2.3
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.3.3
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
- for field in ("key", "id", "name", "path"):
124
- if isinstance(entry.get(field), str):
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 no key/id/name/path field"
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.2.3
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.3.3
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
@@ -1,4 +1,4 @@
1
- assurance-core>=0.3.3
1
+ assurance-core>=0.7
2
2
  openpyxl>=3.1
3
3
 
4
4
  [dev]
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "assurance-cli"
7
- version = "0.2.3"
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.3.3",
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="no key/id/name/path"):
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