@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 +2 -2
- package/package.json +1 -1
- package/uscha-kit/.claude/skills/uscha-devloop/qa_ledger.py +52 -37
- package/uscha-kit/.claude-plugin/plugin.json +1 -1
- package/uscha-kit/.codex-plugin/plugin.json +1 -1
- package/uscha-kit/README.md +1 -1
- package/uscha-kit/VERSION +1 -1
- package/uscha-kit/skills/uscha-devloop/qa_ledger.py +52 -37
- package/uscha-kit/uscha.config.json +1 -1
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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.
|
|
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": {
|
package/uscha-kit/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# uscha-kit
|
|
2
2
|
|
|
3
|
-
**Kit version:** v1.94.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
3712
|
+
repo_path = _scope_path(ledger, args.repo)
|
|
3698
3713
|
|
|
3699
3714
|
def git(*a, **kw):
|
|
3700
3715
|
cwd = kw.pop("cwd", repo_path)
|