@andresmassello/uscha 1.93.0 → 1.93.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.93.0** <!-- uscha:version --> · [uscha.dev](https://uscha.dev) ·
43
+ **Kit v1.93.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.93.0, 53 subcommands, all measured):**
88
+ **What each arrow is, in the engine (kit 1.93.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.93.0",
3
+ "version": "1.93.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",
@@ -780,7 +780,9 @@ def _content_state(repo_path, repo_type, last_snapshot):
780
780
  # verdict, or a report the record itself marked stale, is no anchor at all.
781
781
  if ((snap.get("tests") or {}).get("freshness") or {}).get("status") == "stale":
782
782
  return None
783
- hashes = {r["path"]: r["sha256"]
783
+ # the RECORD, not just the hash string: `_evidence_hash_matches` needs the `sha256_eol`
784
+ # marker beside it to know whether the compatibility comparison is still on offer
785
+ hashes = {r["path"]: r
784
786
  for r in ((snap.get("tests") or {}).get("reports") or [])
785
787
  if isinstance(r, dict) and r.get("path") and r.get("sha256")
786
788
  and r.get("fresh_by") != "stale"}
@@ -814,7 +816,7 @@ def _report_fresh(repo_path, report_path, clock_fresh, content_state):
814
816
  # cannot arise here, while normalizing one side would stop matching the recorded key.
815
817
  rel = os.path.relpath(report_path, repo_path).replace("\\", "/")
816
818
  recorded = content_state["hashes"].get(rel)
817
- if not recorded or _sha256_file(report_path) != recorded:
819
+ if _evidence_hash_matches(report_path, recorded) is not True:
818
820
  return None
819
821
  return "content"
820
822
 
@@ -842,7 +844,10 @@ def _test_evidence_provenance(repo_path, repo_type, last_snapshot=None):
842
844
  "mtime_ns": mtime_ns,
843
845
  "mtime": datetime.fromtimestamp(
844
846
  mtime_ns / 1_000_000_000, timezone.utc).isoformat(),
845
- "sha256": _sha256_file(path),
847
+ "sha256": _sha256_evidence(path),
848
+ # the marker says HOW the hash above was taken, so a reader never has to guess
849
+ # (and 1.93.0-and-older records, which have no marker, keep their compatibility)
850
+ "sha256_eol": "lf",
846
851
  })
847
852
  if not reports:
848
853
  status = "not-applicable" if repo_type == "flutter" else "missing"
@@ -5304,6 +5309,59 @@ def _sha256_file(path):
5304
5309
  return None
5305
5310
 
5306
5311
 
5312
+ # kit 1.93.1: the hash of a TEXT evidence file, EOL-NORMALIZED (CRLF -> LF before hashing).
5313
+ #
5314
+ # A JUnit report is text, and a version control system is allowed to rewrite its line endings
5315
+ # on checkout -- `* text=auto eol=lf` is the recommended `.gitattributes` and the kit's own.
5316
+ # The suite that produced the report on Windows wrote CRLF, the repository stores LF, and a
5317
+ # clean checkout therefore yields a file that is byte-different and semantically identical.
5318
+ # The exact-byte hash read that as `evidence altered after ingest`: 1.93.0 shipped with the
5319
+ # limit merely NAMED in SPEC 4, and the release machine's own board hit it the same day.
5320
+ # Normalizing the ONE difference git is allowed to introduce keeps the guarantee that matters
5321
+ # -- any other changed byte (a swapped log, an edited count, a different run) still fails.
5322
+ #
5323
+ # `_sha256_file` is deliberately left alone: compile-validate hashes MANIFEST UNITS, where the
5324
+ # exact bytes ARE the claim, and a manifest that tolerated a rewrite would be tolerating the
5325
+ # thing it exists to detect.
5326
+ def _sha256_evidence(path):
5327
+ try:
5328
+ with open(path, "rb") as fh:
5329
+ data = fh.read()
5330
+ except OSError:
5331
+ return None
5332
+ return hashlib.sha256(data.replace(b"\r\n", b"\n")).hexdigest()
5333
+
5334
+
5335
+ def _evidence_hash_matches(path, record):
5336
+ """Does the file on disk still hash to what the RECORD carries? True / False, or None when
5337
+ the record carries no hash at all (the caller decides: UNMEASURED, never a pass).
5338
+
5339
+ Records written by 1.93.1 and later carry `sha256_eol: "lf"` and are compared NORMALIZED,
5340
+ full stop. A record written BEFORE that marker hashed the file's exact bytes, whatever
5341
+ line endings the machine that ran the suite happened to write -- and 1.93.0's own release
5342
+ record is the proof: the suite wrote CRLF on Windows, git stored LF, and the LF checkout
5343
+ matched neither the recorded hash nor its normalized form, because the recording was of the
5344
+ CRLF RENDERING of the same text. So a pre-marker record is compared against all three
5345
+ renderings of the bytes on disk: normalized, exact, and CRLF. The door is narrow on purpose
5346
+ -- it opens only for records that predate the marker, and it admits only the line-ending
5347
+ renderings of THIS file's text: a report whose content really changed matches none of them."""
5348
+ want = record.get("sha256") if isinstance(record, dict) else None
5349
+ if not want:
5350
+ return None
5351
+ if _sha256_evidence(path) == want:
5352
+ return True
5353
+ if record.get("sha256_eol") == "lf":
5354
+ return False
5355
+ try:
5356
+ with open(path, "rb") as fh:
5357
+ data = fh.read()
5358
+ except OSError:
5359
+ return False
5360
+ lf = data.replace(b"\r\n", b"\n")
5361
+ return want in (hashlib.sha256(data).hexdigest(),
5362
+ hashlib.sha256(lf.replace(b"\n", b"\r\n")).hexdigest())
5363
+
5364
+
5307
5365
  def _contained_unit(base, unit):
5308
5366
  """A compilation's units must be RELATIVE paths CONTAINED within the compilation
5309
5367
  directory: the manifest references what was compiled, and what was compiled lives with
@@ -8989,7 +9047,7 @@ def _sealed_state(ledger, ledger_path):
8989
9047
  elif not r.get("sha256"):
8990
9048
  unmeasured.append("evidence hash unmeasured: %s — no hash recorded at ingest "
8991
9049
  "(older snapshot, or the file was unreadable)" % rel)
8992
- elif _sha256_file(full) != r["sha256"]:
9050
+ elif _evidence_hash_matches(full, r) is not True:
8993
9051
  failures.append("evidence altered after ingest: %s" % rel)
8994
9052
 
8995
9053
  out["reasons"] = failures + unmeasured
@@ -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.93.0",
4
+ "version": "1.93.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.93.0",
3
+ "version": "1.93.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.93.0 <!-- uscha:version --> · **[uscha.dev](https://uscha.dev)**
3
+ **Kit version:** v1.93.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.93.0
1
+ uscha-kit 1.93.1
@@ -780,7 +780,9 @@ def _content_state(repo_path, repo_type, last_snapshot):
780
780
  # verdict, or a report the record itself marked stale, is no anchor at all.
781
781
  if ((snap.get("tests") or {}).get("freshness") or {}).get("status") == "stale":
782
782
  return None
783
- hashes = {r["path"]: r["sha256"]
783
+ # the RECORD, not just the hash string: `_evidence_hash_matches` needs the `sha256_eol`
784
+ # marker beside it to know whether the compatibility comparison is still on offer
785
+ hashes = {r["path"]: r
784
786
  for r in ((snap.get("tests") or {}).get("reports") or [])
785
787
  if isinstance(r, dict) and r.get("path") and r.get("sha256")
786
788
  and r.get("fresh_by") != "stale"}
@@ -814,7 +816,7 @@ def _report_fresh(repo_path, report_path, clock_fresh, content_state):
814
816
  # cannot arise here, while normalizing one side would stop matching the recorded key.
815
817
  rel = os.path.relpath(report_path, repo_path).replace("\\", "/")
816
818
  recorded = content_state["hashes"].get(rel)
817
- if not recorded or _sha256_file(report_path) != recorded:
819
+ if _evidence_hash_matches(report_path, recorded) is not True:
818
820
  return None
819
821
  return "content"
820
822
 
@@ -842,7 +844,10 @@ def _test_evidence_provenance(repo_path, repo_type, last_snapshot=None):
842
844
  "mtime_ns": mtime_ns,
843
845
  "mtime": datetime.fromtimestamp(
844
846
  mtime_ns / 1_000_000_000, timezone.utc).isoformat(),
845
- "sha256": _sha256_file(path),
847
+ "sha256": _sha256_evidence(path),
848
+ # the marker says HOW the hash above was taken, so a reader never has to guess
849
+ # (and 1.93.0-and-older records, which have no marker, keep their compatibility)
850
+ "sha256_eol": "lf",
846
851
  })
847
852
  if not reports:
848
853
  status = "not-applicable" if repo_type == "flutter" else "missing"
@@ -5304,6 +5309,59 @@ def _sha256_file(path):
5304
5309
  return None
5305
5310
 
5306
5311
 
5312
+ # kit 1.93.1: the hash of a TEXT evidence file, EOL-NORMALIZED (CRLF -> LF before hashing).
5313
+ #
5314
+ # A JUnit report is text, and a version control system is allowed to rewrite its line endings
5315
+ # on checkout -- `* text=auto eol=lf` is the recommended `.gitattributes` and the kit's own.
5316
+ # The suite that produced the report on Windows wrote CRLF, the repository stores LF, and a
5317
+ # clean checkout therefore yields a file that is byte-different and semantically identical.
5318
+ # The exact-byte hash read that as `evidence altered after ingest`: 1.93.0 shipped with the
5319
+ # limit merely NAMED in SPEC 4, and the release machine's own board hit it the same day.
5320
+ # Normalizing the ONE difference git is allowed to introduce keeps the guarantee that matters
5321
+ # -- any other changed byte (a swapped log, an edited count, a different run) still fails.
5322
+ #
5323
+ # `_sha256_file` is deliberately left alone: compile-validate hashes MANIFEST UNITS, where the
5324
+ # exact bytes ARE the claim, and a manifest that tolerated a rewrite would be tolerating the
5325
+ # thing it exists to detect.
5326
+ def _sha256_evidence(path):
5327
+ try:
5328
+ with open(path, "rb") as fh:
5329
+ data = fh.read()
5330
+ except OSError:
5331
+ return None
5332
+ return hashlib.sha256(data.replace(b"\r\n", b"\n")).hexdigest()
5333
+
5334
+
5335
+ def _evidence_hash_matches(path, record):
5336
+ """Does the file on disk still hash to what the RECORD carries? True / False, or None when
5337
+ the record carries no hash at all (the caller decides: UNMEASURED, never a pass).
5338
+
5339
+ Records written by 1.93.1 and later carry `sha256_eol: "lf"` and are compared NORMALIZED,
5340
+ full stop. A record written BEFORE that marker hashed the file's exact bytes, whatever
5341
+ line endings the machine that ran the suite happened to write -- and 1.93.0's own release
5342
+ record is the proof: the suite wrote CRLF on Windows, git stored LF, and the LF checkout
5343
+ matched neither the recorded hash nor its normalized form, because the recording was of the
5344
+ CRLF RENDERING of the same text. So a pre-marker record is compared against all three
5345
+ renderings of the bytes on disk: normalized, exact, and CRLF. The door is narrow on purpose
5346
+ -- it opens only for records that predate the marker, and it admits only the line-ending
5347
+ renderings of THIS file's text: a report whose content really changed matches none of them."""
5348
+ want = record.get("sha256") if isinstance(record, dict) else None
5349
+ if not want:
5350
+ return None
5351
+ if _sha256_evidence(path) == want:
5352
+ return True
5353
+ if record.get("sha256_eol") == "lf":
5354
+ return False
5355
+ try:
5356
+ with open(path, "rb") as fh:
5357
+ data = fh.read()
5358
+ except OSError:
5359
+ return False
5360
+ lf = data.replace(b"\r\n", b"\n")
5361
+ return want in (hashlib.sha256(data).hexdigest(),
5362
+ hashlib.sha256(lf.replace(b"\n", b"\r\n")).hexdigest())
5363
+
5364
+
5307
5365
  def _contained_unit(base, unit):
5308
5366
  """A compilation's units must be RELATIVE paths CONTAINED within the compilation
5309
5367
  directory: the manifest references what was compiled, and what was compiled lives with
@@ -8989,7 +9047,7 @@ def _sealed_state(ledger, ledger_path):
8989
9047
  elif not r.get("sha256"):
8990
9048
  unmeasured.append("evidence hash unmeasured: %s — no hash recorded at ingest "
8991
9049
  "(older snapshot, or the file was unreadable)" % rel)
8992
- elif _sha256_file(full) != r["sha256"]:
9050
+ elif _evidence_hash_matches(full, r) is not True:
8993
9051
  failures.append("evidence altered after ingest: %s" % rel)
8994
9052
 
8995
9053
  out["reasons"] = failures + unmeasured
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.93.0",
2
+ "version": "1.93.1",
3
3
  "project": null,
4
4
  "defaults": {
5
5
  "coverage_threshold": 60,