@andresmassello/uscha 1.56.0 → 1.56.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 +37 -7
- 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 +37 -7
- package/uscha-kit/uscha.config.json +1 -1
package/README.md
CHANGED
|
@@ -40,8 +40,8 @@ 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.56.
|
|
44
|
-
[changelog](https://github.com/andresmassello/uscha/blob/main/uscha-kit/CHANGELOG-1.56.
|
|
43
|
+
**Kit v1.56.1** <!-- uscha:version --> · [uscha.dev](https://uscha.dev) ·
|
|
44
|
+
[changelog](https://github.com/andresmassello/uscha/blob/main/uscha-kit/CHANGELOG-1.56.1.md)
|
|
45
45
|
(the per-release changelogs live in the repo, not in the npm tarball)
|
|
46
46
|
|
|
47
47
|
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andresmassello/uscha",
|
|
3
|
-
"version": "1.56.
|
|
3
|
+
"version": "1.56.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",
|
|
@@ -96,6 +96,36 @@ SOURCE_EXT = {
|
|
|
96
96
|
# --------------------------------------------------------------------------- #
|
|
97
97
|
# ledger io
|
|
98
98
|
# --------------------------------------------------------------------------- #
|
|
99
|
+
# Reports come from the user's build, not from us, and the engine is stdlib-only by contract --
|
|
100
|
+
# `defusedxml` is not available. A byte ceiling is the honest mitigation for the realistic
|
|
101
|
+
# failure (a runaway or hostile report exhausting memory on the operator's own machine). It is
|
|
102
|
+
# NOT protection against a determined attacker: entity expansion inside the ceiling still
|
|
103
|
+
# expands. SECURITY.md says so rather than implying the parser is hardened.
|
|
104
|
+
MAX_REPORT_BYTES = 64 * 1024 * 1024 # 64 MB: orders of magnitude above any real JUnit run
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ReportTooLarge(Exception):
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _parse_xml(source):
|
|
112
|
+
"""ET.parse with a size ceiling. Accepts a path or an open binary/text file object."""
|
|
113
|
+
if hasattr(source, "read"):
|
|
114
|
+
head = source.read(MAX_REPORT_BYTES + 1)
|
|
115
|
+
if len(head) > MAX_REPORT_BYTES:
|
|
116
|
+
raise ReportTooLarge("report exceeds %d bytes" % MAX_REPORT_BYTES)
|
|
117
|
+
if isinstance(head, bytes):
|
|
118
|
+
return ET.ElementTree(ET.fromstring(head))
|
|
119
|
+
return ET.ElementTree(ET.fromstring(head))
|
|
120
|
+
try:
|
|
121
|
+
size = os.path.getsize(str(source))
|
|
122
|
+
except OSError:
|
|
123
|
+
size = 0
|
|
124
|
+
if size > MAX_REPORT_BYTES:
|
|
125
|
+
raise ReportTooLarge("%s exceeds %d bytes" % (source, MAX_REPORT_BYTES))
|
|
126
|
+
return ET.parse(str(source))
|
|
127
|
+
|
|
128
|
+
|
|
99
129
|
def _now():
|
|
100
130
|
return datetime.now(timezone.utc).isoformat(timespec="seconds")
|
|
101
131
|
|
|
@@ -168,7 +198,7 @@ def _repo_cfg(ledger, name):
|
|
|
168
198
|
def _jacoco_line_counter(xml_path):
|
|
169
199
|
"""Return (missed, covered) for the report-level LINE counter."""
|
|
170
200
|
try:
|
|
171
|
-
root =
|
|
201
|
+
root = _parse_xml(xml_path).getroot()
|
|
172
202
|
except ET.ParseError:
|
|
173
203
|
return 0, 0
|
|
174
204
|
for c in root.findall("counter"):
|
|
@@ -237,7 +267,7 @@ def cobertura_coverage(repo_path):
|
|
|
237
267
|
if not path:
|
|
238
268
|
return {"covered": 0, "missed": 0, "pct": 0.0, "report_found": False}
|
|
239
269
|
try:
|
|
240
|
-
root =
|
|
270
|
+
root = _parse_xml(path).getroot()
|
|
241
271
|
except (ET.ParseError, OSError):
|
|
242
272
|
return {"covered": 0, "missed": 0, "pct": 0.0, "report_found": False}
|
|
243
273
|
lc, lv = root.get("lines-covered"), root.get("lines-valid")
|
|
@@ -358,7 +388,7 @@ def _invalid_junit(path, detail):
|
|
|
358
388
|
|
|
359
389
|
def _parse_junit_xml(path):
|
|
360
390
|
try:
|
|
361
|
-
root =
|
|
391
|
+
root = _parse_xml(path).getroot()
|
|
362
392
|
except (ET.ParseError, OSError) as exc:
|
|
363
393
|
_invalid_junit(path, exc)
|
|
364
394
|
root_kind = _local(root.tag)
|
|
@@ -464,7 +494,7 @@ def _perclass_xml_count(patterns, skip_root=None, tolerant=False):
|
|
|
464
494
|
# simply not be ours. Skip it instead of aborting the whole run -- but
|
|
465
495
|
# NEVER silently: every drop is returned so the ledger can surface it.
|
|
466
496
|
try:
|
|
467
|
-
root =
|
|
497
|
+
root = _parse_xml(f).getroot()
|
|
468
498
|
except (ET.ParseError, OSError) as exc:
|
|
469
499
|
dropped.append({"path": f, "reason": f"unreadable XML: {exc}"})
|
|
470
500
|
continue
|
|
@@ -751,7 +781,7 @@ def _ac_tags(repo_path, repo_type):
|
|
|
751
781
|
except OSError:
|
|
752
782
|
pass
|
|
753
783
|
try:
|
|
754
|
-
root =
|
|
784
|
+
root = _parse_xml(f).getroot()
|
|
755
785
|
except (ET.ParseError, OSError):
|
|
756
786
|
continue
|
|
757
787
|
for tc in root.iter():
|
|
@@ -1046,7 +1076,7 @@ def _invalid_static_report(path, label, detail):
|
|
|
1046
1076
|
|
|
1047
1077
|
def _parse_static_xml(path, label, root_name):
|
|
1048
1078
|
try:
|
|
1049
|
-
root =
|
|
1079
|
+
root = _parse_xml(path).getroot()
|
|
1050
1080
|
except (ET.ParseError, OSError) as exc:
|
|
1051
1081
|
_invalid_static_report(path, label, exc)
|
|
1052
1082
|
if _local(root.tag) != root_name:
|
|
@@ -4699,7 +4729,7 @@ def _find_pit_report(path_arg):
|
|
|
4699
4729
|
|
|
4700
4730
|
|
|
4701
4731
|
def _pit_metrics(xml_path):
|
|
4702
|
-
root =
|
|
4732
|
+
root = _parse_xml(xml_path).getroot()
|
|
4703
4733
|
total = killed = survived = no_cov = excluded = 0
|
|
4704
4734
|
by_file = {}
|
|
4705
4735
|
for mut in root.iter("mutation"):
|
|
@@ -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.56.
|
|
4
|
+
"version": "1.56.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, 29 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.56.
|
|
3
|
+
**Kit version:** v1.56.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.56.
|
|
1
|
+
uscha-kit 1.56.1
|
|
@@ -96,6 +96,36 @@ SOURCE_EXT = {
|
|
|
96
96
|
# --------------------------------------------------------------------------- #
|
|
97
97
|
# ledger io
|
|
98
98
|
# --------------------------------------------------------------------------- #
|
|
99
|
+
# Reports come from the user's build, not from us, and the engine is stdlib-only by contract --
|
|
100
|
+
# `defusedxml` is not available. A byte ceiling is the honest mitigation for the realistic
|
|
101
|
+
# failure (a runaway or hostile report exhausting memory on the operator's own machine). It is
|
|
102
|
+
# NOT protection against a determined attacker: entity expansion inside the ceiling still
|
|
103
|
+
# expands. SECURITY.md says so rather than implying the parser is hardened.
|
|
104
|
+
MAX_REPORT_BYTES = 64 * 1024 * 1024 # 64 MB: orders of magnitude above any real JUnit run
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ReportTooLarge(Exception):
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _parse_xml(source):
|
|
112
|
+
"""ET.parse with a size ceiling. Accepts a path or an open binary/text file object."""
|
|
113
|
+
if hasattr(source, "read"):
|
|
114
|
+
head = source.read(MAX_REPORT_BYTES + 1)
|
|
115
|
+
if len(head) > MAX_REPORT_BYTES:
|
|
116
|
+
raise ReportTooLarge("report exceeds %d bytes" % MAX_REPORT_BYTES)
|
|
117
|
+
if isinstance(head, bytes):
|
|
118
|
+
return ET.ElementTree(ET.fromstring(head))
|
|
119
|
+
return ET.ElementTree(ET.fromstring(head))
|
|
120
|
+
try:
|
|
121
|
+
size = os.path.getsize(str(source))
|
|
122
|
+
except OSError:
|
|
123
|
+
size = 0
|
|
124
|
+
if size > MAX_REPORT_BYTES:
|
|
125
|
+
raise ReportTooLarge("%s exceeds %d bytes" % (source, MAX_REPORT_BYTES))
|
|
126
|
+
return ET.parse(str(source))
|
|
127
|
+
|
|
128
|
+
|
|
99
129
|
def _now():
|
|
100
130
|
return datetime.now(timezone.utc).isoformat(timespec="seconds")
|
|
101
131
|
|
|
@@ -168,7 +198,7 @@ def _repo_cfg(ledger, name):
|
|
|
168
198
|
def _jacoco_line_counter(xml_path):
|
|
169
199
|
"""Return (missed, covered) for the report-level LINE counter."""
|
|
170
200
|
try:
|
|
171
|
-
root =
|
|
201
|
+
root = _parse_xml(xml_path).getroot()
|
|
172
202
|
except ET.ParseError:
|
|
173
203
|
return 0, 0
|
|
174
204
|
for c in root.findall("counter"):
|
|
@@ -237,7 +267,7 @@ def cobertura_coverage(repo_path):
|
|
|
237
267
|
if not path:
|
|
238
268
|
return {"covered": 0, "missed": 0, "pct": 0.0, "report_found": False}
|
|
239
269
|
try:
|
|
240
|
-
root =
|
|
270
|
+
root = _parse_xml(path).getroot()
|
|
241
271
|
except (ET.ParseError, OSError):
|
|
242
272
|
return {"covered": 0, "missed": 0, "pct": 0.0, "report_found": False}
|
|
243
273
|
lc, lv = root.get("lines-covered"), root.get("lines-valid")
|
|
@@ -358,7 +388,7 @@ def _invalid_junit(path, detail):
|
|
|
358
388
|
|
|
359
389
|
def _parse_junit_xml(path):
|
|
360
390
|
try:
|
|
361
|
-
root =
|
|
391
|
+
root = _parse_xml(path).getroot()
|
|
362
392
|
except (ET.ParseError, OSError) as exc:
|
|
363
393
|
_invalid_junit(path, exc)
|
|
364
394
|
root_kind = _local(root.tag)
|
|
@@ -464,7 +494,7 @@ def _perclass_xml_count(patterns, skip_root=None, tolerant=False):
|
|
|
464
494
|
# simply not be ours. Skip it instead of aborting the whole run -- but
|
|
465
495
|
# NEVER silently: every drop is returned so the ledger can surface it.
|
|
466
496
|
try:
|
|
467
|
-
root =
|
|
497
|
+
root = _parse_xml(f).getroot()
|
|
468
498
|
except (ET.ParseError, OSError) as exc:
|
|
469
499
|
dropped.append({"path": f, "reason": f"unreadable XML: {exc}"})
|
|
470
500
|
continue
|
|
@@ -751,7 +781,7 @@ def _ac_tags(repo_path, repo_type):
|
|
|
751
781
|
except OSError:
|
|
752
782
|
pass
|
|
753
783
|
try:
|
|
754
|
-
root =
|
|
784
|
+
root = _parse_xml(f).getroot()
|
|
755
785
|
except (ET.ParseError, OSError):
|
|
756
786
|
continue
|
|
757
787
|
for tc in root.iter():
|
|
@@ -1046,7 +1076,7 @@ def _invalid_static_report(path, label, detail):
|
|
|
1046
1076
|
|
|
1047
1077
|
def _parse_static_xml(path, label, root_name):
|
|
1048
1078
|
try:
|
|
1049
|
-
root =
|
|
1079
|
+
root = _parse_xml(path).getroot()
|
|
1050
1080
|
except (ET.ParseError, OSError) as exc:
|
|
1051
1081
|
_invalid_static_report(path, label, exc)
|
|
1052
1082
|
if _local(root.tag) != root_name:
|
|
@@ -4699,7 +4729,7 @@ def _find_pit_report(path_arg):
|
|
|
4699
4729
|
|
|
4700
4730
|
|
|
4701
4731
|
def _pit_metrics(xml_path):
|
|
4702
|
-
root =
|
|
4732
|
+
root = _parse_xml(xml_path).getroot()
|
|
4703
4733
|
total = killed = survived = no_cov = excluded = 0
|
|
4704
4734
|
by_file = {}
|
|
4705
4735
|
for mut in root.iter("mutation"):
|