assurance-cli 0.2.2__tar.gz → 0.3.0__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 (24) hide show
  1. {assurance_cli-0.2.2 → assurance_cli-0.3.0}/PKG-INFO +16 -2
  2. {assurance_cli-0.2.2 → assurance_cli-0.3.0}/README.md +14 -0
  3. assurance_cli-0.3.0/assurance_cli/__init__.py +19 -0
  4. {assurance_cli-0.2.2 → assurance_cli-0.3.0}/assurance_cli/baseline.py +22 -2
  5. {assurance_cli-0.2.2 → assurance_cli-0.3.0}/assurance_cli/cli.py +12 -2
  6. {assurance_cli-0.2.2 → assurance_cli-0.3.0}/assurance_cli/gather.py +18 -5
  7. {assurance_cli-0.2.2 → assurance_cli-0.3.0}/assurance_cli.egg-info/PKG-INFO +16 -2
  8. {assurance_cli-0.2.2 → assurance_cli-0.3.0}/assurance_cli.egg-info/SOURCES.txt +2 -1
  9. {assurance_cli-0.2.2 → assurance_cli-0.3.0}/assurance_cli.egg-info/requires.txt +1 -1
  10. {assurance_cli-0.2.2 → assurance_cli-0.3.0}/pyproject.toml +2 -2
  11. {assurance_cli-0.2.2 → assurance_cli-0.3.0}/tests/test_cli.py +23 -0
  12. assurance_cli-0.3.0/tests/test_version.py +33 -0
  13. assurance_cli-0.2.2/assurance_cli/__init__.py +0 -8
  14. {assurance_cli-0.2.2 → assurance_cli-0.3.0}/LICENSE +0 -0
  15. {assurance_cli-0.2.2 → assurance_cli-0.3.0}/assurance_cli/__main__.py +0 -0
  16. {assurance_cli-0.2.2 → assurance_cli-0.3.0}/assurance_cli/paths.py +0 -0
  17. {assurance_cli-0.2.2 → assurance_cli-0.3.0}/assurance_cli/profile.py +0 -0
  18. {assurance_cli-0.2.2 → assurance_cli-0.3.0}/assurance_cli/setdiff.py +0 -0
  19. {assurance_cli-0.2.2 → assurance_cli-0.3.0}/assurance_cli.egg-info/dependency_links.txt +0 -0
  20. {assurance_cli-0.2.2 → assurance_cli-0.3.0}/assurance_cli.egg-info/entry_points.txt +0 -0
  21. {assurance_cli-0.2.2 → assurance_cli-0.3.0}/assurance_cli.egg-info/top_level.txt +0 -0
  22. {assurance_cli-0.2.2 → assurance_cli-0.3.0}/setup.cfg +0 -0
  23. {assurance_cli-0.2.2 → assurance_cli-0.3.0}/tests/test_read_only.py +0 -0
  24. {assurance_cli-0.2.2 → assurance_cli-0.3.0}/tests/test_setdiff.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: assurance-cli
3
- Version: 0.2.2
3
+ Version: 0.3.0
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.2
22
+ Requires-Dist: assurance-core>=0.4
23
23
  Requires-Dist: openpyxl>=3.1
24
24
  Provides-Extra: dev
25
25
  Requires-Dist: pytest>=8.0; extra == "dev"
@@ -56,6 +56,10 @@ No account · no API key · no network call · **no model decides any of it**
56
56
 
57
57
  ## Three commands
58
58
 
59
+ **`diff` is the general one.** `check` is the special case for a folder of
60
+ dated or numbered *tabular* files — if your files are `.md`, or named in a format it can't read, use
61
+ `diff` and declare the set yourself.
62
+
59
63
  ### `assurance diff` — any two sets of keys
60
64
 
61
65
  ```bash
@@ -95,10 +99,20 @@ assurance check ~/invoices --expect numbered
95
99
  Monthly, quarterly, weekly, daily, numbered. That last line is the **derivation**: it prints with
96
100
  every ratio so you can disagree with the denominator, not just the result.
97
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
+
98
110
  ### `assurance init` — did anything change underneath?
99
111
 
100
112
  ```bash
101
113
  assurance init ~/thesis-data
114
+ # Baseline written to ~/thesis-data/.assurance.json — 34 tabular files recorded.
115
+
102
116
  # ... weeks pass, several people touch the folder ...
103
117
  assurance check ~/thesis-data --against-baseline
104
118
  ```
@@ -29,6 +29,10 @@ No account · no API key · no network call · **no model decides any of it**
29
29
 
30
30
  ## Three commands
31
31
 
32
+ **`diff` is the general one.** `check` is the special case for a folder of
33
+ dated or numbered *tabular* files — if your files are `.md`, or named in a format it can't read, use
34
+ `diff` and declare the set yourself.
35
+
32
36
  ### `assurance diff` — any two sets of keys
33
37
 
34
38
  ```bash
@@ -68,10 +72,20 @@ assurance check ~/invoices --expect numbered
68
72
  Monthly, quarterly, weekly, daily, numbered. That last line is the **derivation**: it prints with
69
73
  every ratio so you can disagree with the denominator, not just the result.
70
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
+
71
83
  ### `assurance init` — did anything change underneath?
72
84
 
73
85
  ```bash
74
86
  assurance init ~/thesis-data
87
+ # Baseline written to ~/thesis-data/.assurance.json — 34 tabular files recorded.
88
+
75
89
  # ... weeks pass, several people touch the folder ...
76
90
  assurance check ~/thesis-data --against-baseline
77
91
  ```
@@ -0,0 +1,19 @@
1
+ """assurance-cli — folder checks anyone can run without I-Ops.
2
+
3
+ Shared filesystem I/O (`paths`, `profile`, `gather`) lives in this package so `assurance-mcp` can
4
+ import it instead of duplicating the profiler. MCP depends on this package for gathering only; the
5
+ CLI does not depend on MCP. Two copies of the profiler is how they disagree.
6
+ """
7
+
8
+ # Read from the installed distribution rather than typed here. A hardcoded literal is a second place
9
+ # the version lives, and the second place is the one that goes stale: these sat at "0.1.0" through
10
+ # five releases and were reported by an outside reviewer on 2026-08-29. One source of truth is
11
+ # `pyproject.toml`, and this reads what was actually installed from it.
12
+ from importlib.metadata import PackageNotFoundError, version as _version
13
+
14
+ try:
15
+ __version__ = _version("assurance-cli")
16
+ except PackageNotFoundError: # running from a source tree that was never installed
17
+ __version__ = "0.0.0+unknown"
18
+
19
+ __all__ = ["__version__"]
@@ -19,7 +19,12 @@ def init_baseline(folder: str, *, update: bool = False) -> dict[str, Any]:
19
19
  root = resolve_folder(folder)
20
20
  path = root / BASELINE_NAME
21
21
  if path.exists() and not update:
22
- return {"written": False, "path": str(path), "reason": "baseline already exists; use --update"}
22
+ return {
23
+ "written": False,
24
+ "path": str(path),
25
+ "reason": "baseline already exists; use --update",
26
+ "summary": f"A baseline already exists at {path}. Pass --update to overwrite it.",
27
+ }
23
28
 
24
29
  entries: dict[str, Any] = {}
25
30
  for file_path in sorted(root.rglob("*")):
@@ -50,7 +55,22 @@ def init_baseline(folder: str, *, update: bool = False) -> dict[str, Any]:
50
55
  "files": entries,
51
56
  }
52
57
  path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
53
- return {"written": True, "path": str(path), "file_count": len(entries)}
58
+ # A summary, because text mode renders one and prints nothing without it. `assurance init`
59
+ # wrote the file and then said nothing at all, so success was indistinguishable from a hung
60
+ # command — reported by an outside reviewer 2026-08-29. The count is the useful half: a baseline
61
+ # over zero files is a baseline that will never notice anything.
62
+ noun = "file" if len(entries) == 1 else "files"
63
+ return {
64
+ "written": True,
65
+ "path": str(path),
66
+ "file_count": len(entries),
67
+ "summary": (
68
+ f"Baseline written to {path} — {len(entries)} tabular {noun} recorded. "
69
+ "Re-check with `assurance check <folder> --against-baseline`."
70
+ if entries
71
+ else f"Baseline written to {path} — no tabular files found, so it records nothing."
72
+ ),
73
+ }
54
74
 
55
75
 
56
76
  def check_against_baseline(folder: str) -> dict[str, Any]:
@@ -14,14 +14,24 @@ from assurance_cli.setdiff import KeySpecError, diff_sets, format_diff
14
14
 
15
15
 
16
16
  def main(argv: list[str] | None = None) -> int:
17
- parser = argparse.ArgumentParser(prog="assurance", description="Folder assurance checks — arithmetic, not models.")
17
+ parser = argparse.ArgumentParser(
18
+ prog="assurance",
19
+ description=(
20
+ "Did the job cover everything it was supposed to cover? Arithmetic, not models. "
21
+ "`diff` is the general command — it compares any two sets of keys. `check` is the "
22
+ "special case for a folder of dated or numbered files."
23
+ ),
24
+ )
18
25
  sub = parser.add_subparsers(dest="command", required=True)
19
26
 
20
27
  init_parser = sub.add_parser("init", help="Write .assurance.json baseline")
21
28
  init_parser.add_argument("folder", help="Folder to baseline")
22
29
  init_parser.add_argument("--update", action="store_true", help="Overwrite an existing baseline")
23
30
 
24
- check_parser = sub.add_parser("check", help="Check a folder for coverage or staleness")
31
+ check_parser = sub.add_parser(
32
+ "check",
33
+ help="Special case: a folder of DATED or NUMBERED files (tabular). For anything else use `diff`",
34
+ )
25
35
  check_parser.add_argument("folder", help="Folder to check")
26
36
  check_parser.add_argument("--expect", choices=["monthly", "quarterly", "weekly", "daily", "numbered"])
27
37
  check_parser.add_argument("--from", dest="from_point", metavar="FROM")
@@ -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,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: assurance-cli
3
- Version: 0.2.2
3
+ Version: 0.3.0
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.2
22
+ Requires-Dist: assurance-core>=0.4
23
23
  Requires-Dist: openpyxl>=3.1
24
24
  Provides-Extra: dev
25
25
  Requires-Dist: pytest>=8.0; extra == "dev"
@@ -56,6 +56,10 @@ No account · no API key · no network call · **no model decides any of it**
56
56
 
57
57
  ## Three commands
58
58
 
59
+ **`diff` is the general one.** `check` is the special case for a folder of
60
+ dated or numbered *tabular* files — if your files are `.md`, or named in a format it can't read, use
61
+ `diff` and declare the set yourself.
62
+
59
63
  ### `assurance diff` — any two sets of keys
60
64
 
61
65
  ```bash
@@ -95,10 +99,20 @@ assurance check ~/invoices --expect numbered
95
99
  Monthly, quarterly, weekly, daily, numbered. That last line is the **derivation**: it prints with
96
100
  every ratio so you can disagree with the denominator, not just the result.
97
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
+
98
110
  ### `assurance init` — did anything change underneath?
99
111
 
100
112
  ```bash
101
113
  assurance init ~/thesis-data
114
+ # Baseline written to ~/thesis-data/.assurance.json — 34 tabular files recorded.
115
+
102
116
  # ... weeks pass, several people touch the folder ...
103
117
  assurance check ~/thesis-data --against-baseline
104
118
  ```
@@ -17,4 +17,5 @@ assurance_cli.egg-info/requires.txt
17
17
  assurance_cli.egg-info/top_level.txt
18
18
  tests/test_cli.py
19
19
  tests/test_read_only.py
20
- tests/test_setdiff.py
20
+ tests/test_setdiff.py
21
+ tests/test_version.py
@@ -1,4 +1,4 @@
1
- assurance-core>=0.3.2
1
+ assurance-core>=0.4
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.2"
7
+ version = "0.3.0"
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.2",
14
+ "assurance-core>=0.4",
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"]
@@ -0,0 +1,33 @@
1
+ """`__version__` must agree with `pyproject.toml`.
2
+
3
+ Both were hardcoded at "0.1.0" and sat there through five releases, so anyone reading
4
+ `assurance_cli.__version__` was told the wrong thing. Reported by an outside reviewer 2026-08-29.
5
+
6
+ They now read the installed distribution's metadata, which makes `pyproject.toml` the only place a
7
+ version is written. This is the guard that the two agree: CI installs the package immediately before
8
+ running the suite, so the metadata is current there. Locally it can lag a version behind after an
9
+ edit — `pip install -e .` resyncs it, and a lagging editable install is exactly the drift worth
10
+ being told about.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import re
16
+ from pathlib import Path
17
+
18
+ import assurance_cli
19
+
20
+
21
+ def _declared() -> str:
22
+ text = (Path(__file__).resolve().parent.parent / "pyproject.toml").read_text(encoding="utf-8")
23
+ match = re.search(r'^version = "([^"]+)"', text, re.MULTILINE)
24
+ assert match, "pyproject.toml has no version"
25
+ return match.group(1)
26
+
27
+
28
+ def test_version_agrees_with_pyproject() -> None:
29
+ assert assurance_cli.__version__ == _declared()
30
+
31
+
32
+ def test_version_is_not_a_placeholder() -> None:
33
+ assert assurance_cli.__version__ != "0.0.0+unknown"
@@ -1,8 +0,0 @@
1
- """assurance-cli — folder checks anyone can run without I-Ops.
2
-
3
- Shared filesystem I/O (`paths`, `profile`, `gather`) lives in this package so `assurance-mcp` can
4
- import it instead of duplicating the profiler. MCP depends on this package for gathering only; the
5
- CLI does not depend on MCP. Two copies of the profiler is how they disagree.
6
- """
7
-
8
- __version__ = "0.1.0"
File without changes
File without changes