@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 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.0** <!-- uscha:version --> · [uscha.dev](https://uscha.dev) ·
44
- [changelog](https://github.com/andresmassello/uscha/blob/main/uscha-kit/CHANGELOG-1.56.0.md)
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.0",
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 = ET.parse(xml_path).getroot()
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 = ET.parse(path).getroot()
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 = ET.parse(path).getroot()
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 = ET.parse(f).getroot()
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 = ET.parse(f).getroot()
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 = ET.parse(path).getroot()
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 = ET.parse(xml_path).getroot()
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.0",
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": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uscha",
3
- "version": "1.56.0",
3
+ "version": "1.56.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.56.0 <!-- uscha:version --> · **[uscha.dev](https://uscha.dev)**
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.0
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 = ET.parse(xml_path).getroot()
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 = ET.parse(path).getroot()
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 = ET.parse(path).getroot()
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 = ET.parse(f).getroot()
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 = ET.parse(f).getroot()
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 = ET.parse(path).getroot()
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 = ET.parse(xml_path).getroot()
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,5 +1,5 @@
1
1
  {
2
- "version": "1.56.0",
2
+ "version": "1.56.1",
3
3
  "project": null,
4
4
  "defaults": {
5
5
  "coverage_threshold": 60,