@andresmassello/uscha 1.60.1 → 1.61.0

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.60.1** <!-- uscha:version --> · [uscha.dev](https://uscha.dev) ·
43
+ **Kit v1.61.0** <!-- 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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andresmassello/uscha",
3
- "version": "1.60.1",
3
+ "version": "1.61.0",
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",
@@ -1858,6 +1858,58 @@ def cmd_init(args):
1858
1858
  f"coverage_threshold={defaults.get('coverage_threshold')})")
1859
1859
 
1860
1860
 
1861
+ def _origin_label(origin):
1862
+ """Render an origin for humans. An unmeasurable tree state reads `unknown`, never
1863
+ `clean` -- the whole reason the field distinguishes False from None."""
1864
+ o = origin or {}
1865
+ sha = (o.get("commit") or "")[:8] or "no-commit"
1866
+ dirty = o.get("dirty")
1867
+ state = "unknown" if dirty is None else ("dirty" if dirty else "clean")
1868
+ return "%s/%s" % (sha, state)
1869
+
1870
+
1871
+ def _evidence_origin(repo_path):
1872
+ """WHERE the evidence came from: the commit it was measured at, and whether the tree
1873
+ was clean (ADR-007). The engine's freshness check compares file MTIMES, so until now a
1874
+ snapshot could say "tests green" without being able to say green AT WHAT -- provenance
1875
+ true of the tree, not of the commit that will merge.
1876
+
1877
+ Absence is NAMED, never guessed: no git, no repo, unreadable -> both None. `dirty is
1878
+ None` must never read as clean; that distinction is the entire point of recording it.
1879
+ Advisory only -- nothing here scores or blocks.
1880
+
1881
+ Untracked files COUNT as dirty (plain --porcelain): an untracked file the suite depends
1882
+ on is exactly the contamination this records, and treating untracked as invisible is a
1883
+ mistake this engine already paid for once (fast-path, 1.57.0)."""
1884
+ origin = {"commit": None, "dirty": None}
1885
+
1886
+ def _git(*args):
1887
+ # OSError covers BOTH ways this used to take the whole snapshot down: git absent
1888
+ # from PATH (FileNotFoundError) and a repo_path that does not exist or is a file
1889
+ # (NotADirectoryError). Every sibling measurement in _snapshot already tolerates a
1890
+ # missing path; provenance must not be the one field that can crash the command it
1891
+ # only annotates. Same posture as _spike_branch. (Both found by fresh review, both
1892
+ # reproduced: an unconfigured or not-yet-cloned repo path is ordinary, not exotic.)
1893
+ try:
1894
+ return subprocess.run(["git"] + list(args), cwd=repo_path,
1895
+ capture_output=True, text=True, encoding="utf-8",
1896
+ errors="replace")
1897
+ except OSError:
1898
+ return None
1899
+
1900
+ rev = _git("rev-parse", "HEAD")
1901
+ if rev is not None and rev.returncode == 0 and rev.stdout.strip():
1902
+ origin["commit"] = rev.stdout.strip()
1903
+ # `-- .` scopes the answer to repo_path. Without it, a repo entry pointing at a
1904
+ # SUBDIRECTORY of a larger working tree reports the whole outer repo's state, so an
1905
+ # unrelated edit elsewhere would mark this repo's evidence dirty -- and the ADR would
1906
+ # be claiming a per-path fact the code did not deliver.
1907
+ st = _git("status", "--porcelain", "--", ".")
1908
+ if st is not None and st.returncode == 0:
1909
+ origin["dirty"] = bool(st.stdout.strip())
1910
+ return origin
1911
+
1912
+
1861
1913
  def _snapshot(ledger, name):
1862
1914
  node = _repo_node(ledger, name)
1863
1915
  cfg = _repo_cfg(ledger, name) if name != "integration" else {"path": ".", "type": "maven"}
@@ -1868,6 +1920,7 @@ def _snapshot(ledger, name):
1868
1920
  "coverage": coverage(path, rtype),
1869
1921
  "tests": test_count(path, rtype),
1870
1922
  "loc": count_loc(path, rtype),
1923
+ "origin": _evidence_origin(path),
1871
1924
  }
1872
1925
  node["snapshots"].append(snap)
1873
1926
  return snap
@@ -1887,6 +1940,7 @@ def cmd_snapshot(args):
1887
1940
  f"coverage={cov['pct']}% (found={cov['report_found']}), "
1888
1941
  f"tests={tests['total']} (found={tests['report_found']}), "
1889
1942
  f"freshness={freshness.get('status', 'unknown')}, "
1943
+ f"origin={_origin_label(snap.get('origin'))}, "
1890
1944
  f"prod_loc={loc['prod_loc']}, test_loc={loc['test_loc']}")
1891
1945
  if freshness.get("status") == "stale":
1892
1946
  print(f" test evidence stale: {freshness.get('reason')}")
@@ -4319,6 +4373,16 @@ def cmd_dashboard(args):
4319
4373
  for r in {e.get("repo") for e in ledger["fast_path"]}}
4320
4374
  if ledger.get("spec_drift"):
4321
4375
  out["spec_drift"] = ledger["spec_drift"]
4376
+ # evidence_origin: the latest snapshot's origin per repo, and ONLY when one exists --
4377
+ # a ledger predating ADR-007 keeps the exact prior schema (same conditional-key rule
4378
+ # fast_path and spec_drift already follow).
4379
+ _org = {}
4380
+ for _rn, _rnode in ledger["repos"].items():
4381
+ _snaps = _rnode.get("snapshots") or []
4382
+ if _snaps and _snaps[-1].get("origin"):
4383
+ _org[_rn] = _snaps[-1]["origin"]
4384
+ if _org:
4385
+ out["evidence_origin"] = _org
4322
4386
  if getattr(args, "json", False):
4323
4387
  print(json.dumps(out, indent=2, ensure_ascii=False))
4324
4388
  return
@@ -90,6 +90,12 @@ Line guide:
90
90
  block with the latest verdict per repo (`fast-path: ALLOW (intent...)` / `ESCALATED`). Absent
91
91
  entries → no line at all: silence is honest when no mode was requested.
92
92
 
93
+ **Evidence origin (ADR-007):** if the latest snapshot was measured on a DIRTY tree, add ONE
94
+ line: `evidence: measured dirty at <sha8> - not from the commit alone`. Say nothing when the
95
+ tree was clean (the normal case needs no words) and nothing when it is `null` (unmeasurable,
96
+ and inventing a state is worse than silence). This never explains a blocked phase: it scores
97
+ nothing and gates nothing.
98
+
93
99
  **Spec-drift (ADR-005):** if the ledger carries a `spec_drift` run, add ONE line:
94
100
  `spec-drift: N stale / M docs (advisory)` — or `spec-drift: no drift measured` when zero are
95
101
  stale. Always label it advisory; it never explains a blocked phase. Absent key → no line.
@@ -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.60.1",
4
+ "version": "1.61.0",
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, 32 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.60.1",
3
+ "version": "1.61.0",
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.60.1 <!-- uscha:version --> · **[uscha.dev](https://uscha.dev)**
3
+ **Kit version:** v1.61.0 <!-- 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`,
@@ -127,6 +127,27 @@ spec has no commit date to compare. The latest run lands in the ledger (`spec_dr
127
127
  mirador can surface it. No readiness impact, no exit-code gate: a stale spec is a prompt for
128
128
  a human conversation, not a blocked pipeline.
129
129
 
130
+ ## Evidence origin (ADR-007) - green, but green at *what*?
131
+
132
+ Freshness compares file mtimes, so the ledger could say "tests green" without being able to
133
+ say which commit that was true of. Every snapshot now stamps where it came from:
134
+
135
+ ```json
136
+ "origin": { "commit": "5d17cf4...", "dirty": false }
137
+ ```
138
+
139
+ Measured with `git rev-parse HEAD` and `git status --porcelain` in the repo path. **Untracked
140
+ files count as dirty** - an untracked file the suite depends on is exactly the contamination
141
+ worth recording. **No git, no repo -> both `null`**, and `dirty: null` never reads as clean:
142
+ a tree state nobody could measure is not a clean one.
143
+
144
+ Advisory throughout: `snapshot` prints `origin=<sha8>/<clean|dirty|unknown>`,
145
+ `dashboard --json` carries `evidence_origin` when a snapshot has one, `/uscha-status` says one
146
+ line when the latest evidence was dirty. Readiness, phase and convergence are untouched -
147
+ knowing a tree was dirty does not tell you the evidence is wrong, only that it was not
148
+ produced from a commit alone. The git-worktree clean-room that would answer the stronger
149
+ question is deliberately deferred; ADR-007 records why.
150
+
130
151
  ## End-to-end flow
131
152
 
132
153
  `uscha-discovery` is the front for something new (you only have the idea); `uscha-adr-refine` is the front
package/uscha-kit/VERSION CHANGED
@@ -1 +1 @@
1
- uscha-kit 1.60.1
1
+ uscha-kit 1.61.0
@@ -0,0 +1 @@
1
+ {"AC-EP-01": true, "AC-EP-02": true, "AC-EP-03": true, "AC-EP-05": true, "AC-EP-04": true}
@@ -1858,6 +1858,58 @@ def cmd_init(args):
1858
1858
  f"coverage_threshold={defaults.get('coverage_threshold')})")
1859
1859
 
1860
1860
 
1861
+ def _origin_label(origin):
1862
+ """Render an origin for humans. An unmeasurable tree state reads `unknown`, never
1863
+ `clean` -- the whole reason the field distinguishes False from None."""
1864
+ o = origin or {}
1865
+ sha = (o.get("commit") or "")[:8] or "no-commit"
1866
+ dirty = o.get("dirty")
1867
+ state = "unknown" if dirty is None else ("dirty" if dirty else "clean")
1868
+ return "%s/%s" % (sha, state)
1869
+
1870
+
1871
+ def _evidence_origin(repo_path):
1872
+ """WHERE the evidence came from: the commit it was measured at, and whether the tree
1873
+ was clean (ADR-007). The engine's freshness check compares file MTIMES, so until now a
1874
+ snapshot could say "tests green" without being able to say green AT WHAT -- provenance
1875
+ true of the tree, not of the commit that will merge.
1876
+
1877
+ Absence is NAMED, never guessed: no git, no repo, unreadable -> both None. `dirty is
1878
+ None` must never read as clean; that distinction is the entire point of recording it.
1879
+ Advisory only -- nothing here scores or blocks.
1880
+
1881
+ Untracked files COUNT as dirty (plain --porcelain): an untracked file the suite depends
1882
+ on is exactly the contamination this records, and treating untracked as invisible is a
1883
+ mistake this engine already paid for once (fast-path, 1.57.0)."""
1884
+ origin = {"commit": None, "dirty": None}
1885
+
1886
+ def _git(*args):
1887
+ # OSError covers BOTH ways this used to take the whole snapshot down: git absent
1888
+ # from PATH (FileNotFoundError) and a repo_path that does not exist or is a file
1889
+ # (NotADirectoryError). Every sibling measurement in _snapshot already tolerates a
1890
+ # missing path; provenance must not be the one field that can crash the command it
1891
+ # only annotates. Same posture as _spike_branch. (Both found by fresh review, both
1892
+ # reproduced: an unconfigured or not-yet-cloned repo path is ordinary, not exotic.)
1893
+ try:
1894
+ return subprocess.run(["git"] + list(args), cwd=repo_path,
1895
+ capture_output=True, text=True, encoding="utf-8",
1896
+ errors="replace")
1897
+ except OSError:
1898
+ return None
1899
+
1900
+ rev = _git("rev-parse", "HEAD")
1901
+ if rev is not None and rev.returncode == 0 and rev.stdout.strip():
1902
+ origin["commit"] = rev.stdout.strip()
1903
+ # `-- .` scopes the answer to repo_path. Without it, a repo entry pointing at a
1904
+ # SUBDIRECTORY of a larger working tree reports the whole outer repo's state, so an
1905
+ # unrelated edit elsewhere would mark this repo's evidence dirty -- and the ADR would
1906
+ # be claiming a per-path fact the code did not deliver.
1907
+ st = _git("status", "--porcelain", "--", ".")
1908
+ if st is not None and st.returncode == 0:
1909
+ origin["dirty"] = bool(st.stdout.strip())
1910
+ return origin
1911
+
1912
+
1861
1913
  def _snapshot(ledger, name):
1862
1914
  node = _repo_node(ledger, name)
1863
1915
  cfg = _repo_cfg(ledger, name) if name != "integration" else {"path": ".", "type": "maven"}
@@ -1868,6 +1920,7 @@ def _snapshot(ledger, name):
1868
1920
  "coverage": coverage(path, rtype),
1869
1921
  "tests": test_count(path, rtype),
1870
1922
  "loc": count_loc(path, rtype),
1923
+ "origin": _evidence_origin(path),
1871
1924
  }
1872
1925
  node["snapshots"].append(snap)
1873
1926
  return snap
@@ -1887,6 +1940,7 @@ def cmd_snapshot(args):
1887
1940
  f"coverage={cov['pct']}% (found={cov['report_found']}), "
1888
1941
  f"tests={tests['total']} (found={tests['report_found']}), "
1889
1942
  f"freshness={freshness.get('status', 'unknown')}, "
1943
+ f"origin={_origin_label(snap.get('origin'))}, "
1890
1944
  f"prod_loc={loc['prod_loc']}, test_loc={loc['test_loc']}")
1891
1945
  if freshness.get("status") == "stale":
1892
1946
  print(f" test evidence stale: {freshness.get('reason')}")
@@ -4319,6 +4373,16 @@ def cmd_dashboard(args):
4319
4373
  for r in {e.get("repo") for e in ledger["fast_path"]}}
4320
4374
  if ledger.get("spec_drift"):
4321
4375
  out["spec_drift"] = ledger["spec_drift"]
4376
+ # evidence_origin: the latest snapshot's origin per repo, and ONLY when one exists --
4377
+ # a ledger predating ADR-007 keeps the exact prior schema (same conditional-key rule
4378
+ # fast_path and spec_drift already follow).
4379
+ _org = {}
4380
+ for _rn, _rnode in ledger["repos"].items():
4381
+ _snaps = _rnode.get("snapshots") or []
4382
+ if _snaps and _snaps[-1].get("origin"):
4383
+ _org[_rn] = _snaps[-1]["origin"]
4384
+ if _org:
4385
+ out["evidence_origin"] = _org
4322
4386
  if getattr(args, "json", False):
4323
4387
  print(json.dumps(out, indent=2, ensure_ascii=False))
4324
4388
  return
@@ -90,6 +90,12 @@ Line guide:
90
90
  block with the latest verdict per repo (`fast-path: ALLOW (intent...)` / `ESCALATED`). Absent
91
91
  entries → no line at all: silence is honest when no mode was requested.
92
92
 
93
+ **Evidence origin (ADR-007):** if the latest snapshot was measured on a DIRTY tree, add ONE
94
+ line: `evidence: measured dirty at <sha8> - not from the commit alone`. Say nothing when the
95
+ tree was clean (the normal case needs no words) and nothing when it is `null` (unmeasurable,
96
+ and inventing a state is worse than silence). This never explains a blocked phase: it scores
97
+ nothing and gates nothing.
98
+
93
99
  **Spec-drift (ADR-005):** if the ledger carries a `spec_drift` run, add ONE line:
94
100
  `spec-drift: N stale / M docs (advisory)` — or `spec-drift: no drift measured` when zero are
95
101
  stale. Always label it advisory; it never explains a blocked phase. Absent key → no line.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.60.1",
2
+ "version": "1.61.0",
3
3
  "project": null,
4
4
  "defaults": {
5
5
  "coverage_threshold": 60,