@andresmassello/uscha 1.94.0 → 1.94.1

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.
package/README.md CHANGED
@@ -40,7 +40,7 @@ Requires **Python 3.8+** on the machine (the engine is Python stdlib — no pip
40
40
  runtime dependencies). The npm package is a thin router; the canonical installer is
41
41
  `uscha-kit/install-uscha.py`.
42
42
 
43
- **Kit v1.94.0** <!-- uscha:version --> · [uscha.dev](https://uscha.dev) ·
43
+ **Kit v1.94.1** <!-- uscha:version --> · [uscha.dev](https://uscha.dev) ·
44
44
  [changelog](https://github.com/andresmassello/uscha/blob/main/uscha-kit/CHANGELOG.md)
45
45
  (the per-release changelogs live in the repo, not in the npm tarball)
46
46
 
@@ -85,7 +85,7 @@ automatic tool can perform: a human verdict.
85
85
  from the compiled code: 0.828 measured (12 archetypes) — names AND behaviour
86
86
  ```
87
87
 
88
- **What each arrow is, in the engine (kit 1.94.0, 53 subcommands, all measured):**
88
+ **What each arrow is, in the engine (kit 1.94.1, 53 subcommands, all measured):**
89
89
 
90
90
  | Leg | Subcommands | What it establishes |
91
91
  |---|---|---|
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andresmassello/uscha",
3
- "version": "1.94.0",
3
+ "version": "1.94.1",
4
4
  "description": "Spec-driven development for LLM coding agents: 9 skills + a stdlib evidence engine. Facts block, guesses advise; the human approves.",
5
5
  "author": {
6
6
  "name": "Andres Massello",
@@ -206,17 +206,51 @@ def _repo_cfg(ledger, name):
206
206
  # measurement: coverage
207
207
  # --------------------------------------------------------------------------- #
208
208
  def _jacoco_line_counter(xml_path):
209
- """Return (missed, covered) for the report-level LINE counter."""
209
+ """Return (missed, covered) for the report-level LINE counter, or None when the report
210
+ could not be READ. The distinction is the whole point: a report that measured nothing
211
+ must never be summed as (0, 0) — that is an invented number, and with one module's XML
212
+ truncated mid-write it made the surviving modules' percentage look like the project's
213
+ (audit 1.94.1). Absence is never invented as a number — the rule `cobertura_coverage`
214
+ already states, applied to the JaCoCo readers too. A report with no LINE counter at all
215
+ is a real, readable zero and stays (0, 0)."""
210
216
  try:
211
217
  root = _parse_xml(xml_path).getroot()
212
- except ET.ParseError:
213
- return 0, 0
218
+ except (ET.ParseError, OSError, ReportTooLarge):
219
+ return None
214
220
  for c in root.findall("counter"):
215
221
  if c.get("type") == "LINE":
216
- return int(c.get("missed", 0)), int(c.get("covered", 0))
222
+ try:
223
+ return int(c.get("missed", 0)), int(c.get("covered", 0))
224
+ except (TypeError, ValueError):
225
+ return None # a counter that is not a number measured nothing
217
226
  return 0, 0
218
227
 
219
228
 
229
+ def _jacoco_result(files):
230
+ """Sum the LINE counters of `files`, or report the WHOLE reading unmeasured. Shared by
231
+ maven/gradle/ant: three hand-copied versions of this sum is how one of them would drift.
232
+
233
+ ONE unreadable report makes the reading unmeasured, and an unmeasured reading is
234
+ (0, 0, 0.0) -- not the survivors' percentage with a false flag beside it. Every other
235
+ reader in this file returns pct 0.0 whenever report_found is False, and readers of the
236
+ result rely on it: `readiness` scores coverage as pct/threshold WITHOUT consulting
237
+ report_found, and `snapshot` prints and persists the pct next to the flag. Handing them
238
+ the surviving modules' number would score a project on a question nobody asked
239
+ (audit 1.94.1). The report paths are still listed -- the operator needs to know which
240
+ files were looked at."""
241
+ parsed = [_jacoco_line_counter(f) for f in files]
242
+ reports = [f.replace("\\", "/") for f in files]
243
+ if not files or any(p is None for p in parsed):
244
+ return {"covered": 0, "missed": 0, "pct": 0.0, "report_found": False,
245
+ "reports": reports}
246
+ missed = sum(p[0] for p in parsed)
247
+ covered = sum(p[1] for p in parsed)
248
+ total = missed + covered
249
+ pct = round(covered / total * 100, 2) if total else 0.0
250
+ return {"covered": covered, "missed": missed, "pct": pct,
251
+ "report_found": True, "reports": reports}
252
+
253
+
220
254
  def maven_coverage(repo_path):
221
255
  """
222
256
  Prefer an aggregate report if present, else sum per-module reports.
@@ -231,16 +265,7 @@ def maven_coverage(repo_path):
231
265
  files = glob.glob(os.path.join(repo_path, "**", "target", "site",
232
266
  "jacoco", "jacoco.xml"),
233
267
  recursive=True)
234
- missed = covered = 0
235
- for f in files:
236
- m, c = _jacoco_line_counter(f)
237
- missed += m
238
- covered += c
239
- total = missed + covered
240
- pct = round(covered / total * 100, 2) if total else 0.0
241
- return {"covered": covered, "missed": missed, "pct": pct,
242
- "report_found": bool(files),
243
- "reports": [f.replace("\\", "/") for f in files]}
268
+ return _jacoco_result(files)
244
269
 
245
270
 
246
271
  def flutter_coverage(repo_path):
@@ -343,16 +368,7 @@ def gradle_coverage(repo_path):
343
368
  if not files:
344
369
  files = glob.glob(os.path.join(repo_path, "**", "build", "reports",
345
370
  "jacoco", "jacoco.xml"), recursive=True)
346
- missed = covered = 0
347
- for f in files:
348
- m, c = _jacoco_line_counter(f)
349
- missed += m
350
- covered += c
351
- total = missed + covered
352
- pct = round(covered / total * 100, 2) if total else 0.0
353
- return {"covered": covered, "missed": missed, "pct": pct,
354
- "report_found": bool(files),
355
- "reports": [f.replace("\\", "/") for f in files]}
371
+ return _jacoco_result(files)
356
372
 
357
373
 
358
374
  def ant_coverage(repo_path):
@@ -360,16 +376,7 @@ def ant_coverage(repo_path):
360
376
  report task writes wherever the build file says -- so the report is discovered
361
377
  RECURSIVELY by name instead of guessing one convention."""
362
378
  files = _ant_reports(repo_path, "jacoco.xml")
363
- missed = covered = 0
364
- for f in files:
365
- m, c = _jacoco_line_counter(f)
366
- missed += m
367
- covered += c
368
- total = missed + covered
369
- pct = round(covered / total * 100, 2) if total else 0.0
370
- return {"covered": covered, "missed": missed, "pct": pct,
371
- "report_found": bool(files),
372
- "reports": [f.replace("\\", "/") for f in files]}
379
+ return _jacoco_result(files)
373
380
 
374
381
 
375
382
  def coverage(repo_path, repo_type):
@@ -1276,7 +1283,15 @@ def _mk_id(tool, rule, fname, line, granularity):
1276
1283
 
1277
1284
  def _find_all(base, patterns, explicit):
1278
1285
  if explicit:
1279
- return [explicit] if os.path.exists(explicit) else []
1286
+ if not os.path.exists(explicit):
1287
+ # An EXPLICIT path is a claim the operator made about where the report is. When
1288
+ # it is not there, the honest reading is a typo or a build that never wrote it --
1289
+ # not "this linter has no findings". Returning [] made ingest-gate log nothing
1290
+ # and exit 0, so a mistyped --ruff read as a clean gate (audit 1.94.1). Same
1291
+ # fail-closed exit an unparseable report already gets, with the path named.
1292
+ _invalid_static_report(explicit, "linter",
1293
+ "no such file (explicit path given, nothing to ingest)")
1294
+ return [explicit]
1280
1295
  found = []
1281
1296
  for pat in patterns:
1282
1297
  found += glob.glob(os.path.join(base, pat), recursive=True)
@@ -3107,7 +3122,7 @@ def cmd_fastpath_eval(args):
3107
3122
  sig("configured", False, "defaults.fast_path present and enabled",
3108
3123
  "config.defaults.fast_path", False)
3109
3124
  else:
3110
- repo_path = _repo_cfg(ledger, args.repo).get("path", ".")
3125
+ repo_path = _scope_path(ledger, args.repo)
3111
3126
  base, base_src = args.base, "--base"
3112
3127
  if base:
3113
3128
  probe = subprocess.run(["git", "rev-parse", "--verify", base + "^{commit}"],
@@ -3694,7 +3709,7 @@ def cmd_cleanroom(args):
3694
3709
  import time
3695
3710
  ledger = _load(args.ledger)
3696
3711
  _repo_node(ledger, args.repo)
3697
- repo_path = _repo_cfg(ledger, args.repo).get("path", ".")
3712
+ repo_path = _scope_path(ledger, args.repo)
3698
3713
 
3699
3714
  def git(*a, **kw):
3700
3715
  cwd = kw.pop("cwd", repo_path)
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "uscha",
4
- "version": "1.94.0",
4
+ "version": "1.94.1",
5
5
  "displayName": "Uscha",
6
6
  "description": "Spec-driven development for LLM coding agents: 9 skills (discovery, adr-refine, reverse-discovery, characterize, devloop, sysdoc, rubric, mirador, status) + a stdlib measurement engine (qa_ledger.py, 53 subcommands + universal installer + npm/npx router). Facts block, guesses advise; the human approves.",
7
7
  "author": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uscha",
3
- "version": "1.94.0",
3
+ "version": "1.94.1",
4
4
  "description": "Uscha spec-driven development methodology for coding agents. Includes npm/npx router.",
5
5
  "author": {
6
6
  "name": "Andres Massello",
@@ -1,6 +1,6 @@
1
1
  # uscha-kit
2
2
 
3
- **Kit version:** v1.94.0 <!-- uscha:version --> · **[uscha.dev](https://uscha.dev)**
3
+ **Kit version:** v1.94.1 <!-- uscha:version --> · **[uscha.dev](https://uscha.dev)**
4
4
 
5
5
  Spec-driven orchestrator + multi-repo QA for Claude Code, with a deterministic ledger.
6
6
  **Nine skills** (`uscha-discovery`, `uscha-adr-refine`, `uscha-devloop`, `uscha-sysdoc`, `uscha-reverse-discovery`,
package/uscha-kit/VERSION CHANGED
@@ -1 +1 @@
1
- uscha-kit 1.94.0
1
+ uscha-kit 1.94.1
@@ -206,17 +206,51 @@ def _repo_cfg(ledger, name):
206
206
  # measurement: coverage
207
207
  # --------------------------------------------------------------------------- #
208
208
  def _jacoco_line_counter(xml_path):
209
- """Return (missed, covered) for the report-level LINE counter."""
209
+ """Return (missed, covered) for the report-level LINE counter, or None when the report
210
+ could not be READ. The distinction is the whole point: a report that measured nothing
211
+ must never be summed as (0, 0) — that is an invented number, and with one module's XML
212
+ truncated mid-write it made the surviving modules' percentage look like the project's
213
+ (audit 1.94.1). Absence is never invented as a number — the rule `cobertura_coverage`
214
+ already states, applied to the JaCoCo readers too. A report with no LINE counter at all
215
+ is a real, readable zero and stays (0, 0)."""
210
216
  try:
211
217
  root = _parse_xml(xml_path).getroot()
212
- except ET.ParseError:
213
- return 0, 0
218
+ except (ET.ParseError, OSError, ReportTooLarge):
219
+ return None
214
220
  for c in root.findall("counter"):
215
221
  if c.get("type") == "LINE":
216
- return int(c.get("missed", 0)), int(c.get("covered", 0))
222
+ try:
223
+ return int(c.get("missed", 0)), int(c.get("covered", 0))
224
+ except (TypeError, ValueError):
225
+ return None # a counter that is not a number measured nothing
217
226
  return 0, 0
218
227
 
219
228
 
229
+ def _jacoco_result(files):
230
+ """Sum the LINE counters of `files`, or report the WHOLE reading unmeasured. Shared by
231
+ maven/gradle/ant: three hand-copied versions of this sum is how one of them would drift.
232
+
233
+ ONE unreadable report makes the reading unmeasured, and an unmeasured reading is
234
+ (0, 0, 0.0) -- not the survivors' percentage with a false flag beside it. Every other
235
+ reader in this file returns pct 0.0 whenever report_found is False, and readers of the
236
+ result rely on it: `readiness` scores coverage as pct/threshold WITHOUT consulting
237
+ report_found, and `snapshot` prints and persists the pct next to the flag. Handing them
238
+ the surviving modules' number would score a project on a question nobody asked
239
+ (audit 1.94.1). The report paths are still listed -- the operator needs to know which
240
+ files were looked at."""
241
+ parsed = [_jacoco_line_counter(f) for f in files]
242
+ reports = [f.replace("\\", "/") for f in files]
243
+ if not files or any(p is None for p in parsed):
244
+ return {"covered": 0, "missed": 0, "pct": 0.0, "report_found": False,
245
+ "reports": reports}
246
+ missed = sum(p[0] for p in parsed)
247
+ covered = sum(p[1] for p in parsed)
248
+ total = missed + covered
249
+ pct = round(covered / total * 100, 2) if total else 0.0
250
+ return {"covered": covered, "missed": missed, "pct": pct,
251
+ "report_found": True, "reports": reports}
252
+
253
+
220
254
  def maven_coverage(repo_path):
221
255
  """
222
256
  Prefer an aggregate report if present, else sum per-module reports.
@@ -231,16 +265,7 @@ def maven_coverage(repo_path):
231
265
  files = glob.glob(os.path.join(repo_path, "**", "target", "site",
232
266
  "jacoco", "jacoco.xml"),
233
267
  recursive=True)
234
- missed = covered = 0
235
- for f in files:
236
- m, c = _jacoco_line_counter(f)
237
- missed += m
238
- covered += c
239
- total = missed + covered
240
- pct = round(covered / total * 100, 2) if total else 0.0
241
- return {"covered": covered, "missed": missed, "pct": pct,
242
- "report_found": bool(files),
243
- "reports": [f.replace("\\", "/") for f in files]}
268
+ return _jacoco_result(files)
244
269
 
245
270
 
246
271
  def flutter_coverage(repo_path):
@@ -343,16 +368,7 @@ def gradle_coverage(repo_path):
343
368
  if not files:
344
369
  files = glob.glob(os.path.join(repo_path, "**", "build", "reports",
345
370
  "jacoco", "jacoco.xml"), recursive=True)
346
- missed = covered = 0
347
- for f in files:
348
- m, c = _jacoco_line_counter(f)
349
- missed += m
350
- covered += c
351
- total = missed + covered
352
- pct = round(covered / total * 100, 2) if total else 0.0
353
- return {"covered": covered, "missed": missed, "pct": pct,
354
- "report_found": bool(files),
355
- "reports": [f.replace("\\", "/") for f in files]}
371
+ return _jacoco_result(files)
356
372
 
357
373
 
358
374
  def ant_coverage(repo_path):
@@ -360,16 +376,7 @@ def ant_coverage(repo_path):
360
376
  report task writes wherever the build file says -- so the report is discovered
361
377
  RECURSIVELY by name instead of guessing one convention."""
362
378
  files = _ant_reports(repo_path, "jacoco.xml")
363
- missed = covered = 0
364
- for f in files:
365
- m, c = _jacoco_line_counter(f)
366
- missed += m
367
- covered += c
368
- total = missed + covered
369
- pct = round(covered / total * 100, 2) if total else 0.0
370
- return {"covered": covered, "missed": missed, "pct": pct,
371
- "report_found": bool(files),
372
- "reports": [f.replace("\\", "/") for f in files]}
379
+ return _jacoco_result(files)
373
380
 
374
381
 
375
382
  def coverage(repo_path, repo_type):
@@ -1276,7 +1283,15 @@ def _mk_id(tool, rule, fname, line, granularity):
1276
1283
 
1277
1284
  def _find_all(base, patterns, explicit):
1278
1285
  if explicit:
1279
- return [explicit] if os.path.exists(explicit) else []
1286
+ if not os.path.exists(explicit):
1287
+ # An EXPLICIT path is a claim the operator made about where the report is. When
1288
+ # it is not there, the honest reading is a typo or a build that never wrote it --
1289
+ # not "this linter has no findings". Returning [] made ingest-gate log nothing
1290
+ # and exit 0, so a mistyped --ruff read as a clean gate (audit 1.94.1). Same
1291
+ # fail-closed exit an unparseable report already gets, with the path named.
1292
+ _invalid_static_report(explicit, "linter",
1293
+ "no such file (explicit path given, nothing to ingest)")
1294
+ return [explicit]
1280
1295
  found = []
1281
1296
  for pat in patterns:
1282
1297
  found += glob.glob(os.path.join(base, pat), recursive=True)
@@ -3107,7 +3122,7 @@ def cmd_fastpath_eval(args):
3107
3122
  sig("configured", False, "defaults.fast_path present and enabled",
3108
3123
  "config.defaults.fast_path", False)
3109
3124
  else:
3110
- repo_path = _repo_cfg(ledger, args.repo).get("path", ".")
3125
+ repo_path = _scope_path(ledger, args.repo)
3111
3126
  base, base_src = args.base, "--base"
3112
3127
  if base:
3113
3128
  probe = subprocess.run(["git", "rev-parse", "--verify", base + "^{commit}"],
@@ -3694,7 +3709,7 @@ def cmd_cleanroom(args):
3694
3709
  import time
3695
3710
  ledger = _load(args.ledger)
3696
3711
  _repo_node(ledger, args.repo)
3697
- repo_path = _repo_cfg(ledger, args.repo).get("path", ".")
3712
+ repo_path = _scope_path(ledger, args.repo)
3698
3713
 
3699
3714
  def git(*a, **kw):
3700
3715
  cwd = kw.pop("cwd", repo_path)
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.94.0",
2
+ "version": "1.94.1",
3
3
  "project": null,
4
4
  "defaults": {
5
5
  "coverage_threshold": 60,