@andresmassello/uscha 1.59.0 → 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 +2 -2
- package/bin/uscha.js +0 -0
- package/package.json +1 -1
- package/uscha-kit/.claude/skills/uscha-characterize/SKILL.md +18 -0
- package/uscha-kit/.claude/skills/uscha-devloop/qa_ledger.py +283 -0
- package/uscha-kit/.claude/skills/uscha-mirador/mirador-watch.ps1 +22 -22
- package/uscha-kit/.claude/skills/uscha-mirador/mirador-watch.sh +0 -0
- package/uscha-kit/.claude/skills/uscha-status/SKILL.md +6 -0
- package/uscha-kit/.claude-plugin/plugin.json +2 -2
- package/uscha-kit/.codex-plugin/plugin.json +1 -1
- package/uscha-kit/INSTALL.md +128 -128
- package/uscha-kit/README.md +58 -1
- package/uscha-kit/VERSION +1 -1
- package/uscha-kit/hooks/block-approved-writes.py +0 -0
- package/uscha-kit/reports/junit/.goldencov-cases.json +1 -0
- package/uscha-kit/reports/junit/.origin-cases.json +1 -0
- package/uscha-kit/skills/uscha-characterize/SKILL.md +18 -0
- package/uscha-kit/skills/uscha-devloop/qa_ledger.py +283 -0
- package/uscha-kit/skills/uscha-mirador/mirador-watch.ps1 +22 -22
- package/uscha-kit/skills/uscha-mirador/mirador-watch.sh +0 -0
- package/uscha-kit/skills/uscha-status/SKILL.md +6 -0
- package/uscha-kit/uscha.config.json +3 -2
- package/uscha-kit/workbench-doctor.sh +0 -0
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.
|
|
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
|
|
|
@@ -76,7 +76,7 @@ and see which file, which test, and when.
|
|
|
76
76
|
| `/uscha-mirador` | Bird's-eye HTML dashboard: readiness, trail, acceptance, loops |
|
|
77
77
|
| `/uscha-status` | One-line progress readout, in chat |
|
|
78
78
|
|
|
79
|
-
**A measurement engine** (`qa_ledger.py`,
|
|
79
|
+
**A measurement engine** (`qa_ledger.py`, 32 subcommands, Python stdlib) that ingests
|
|
80
80
|
evidence from **11 language stacks** — maven, gradle, ant, python, node, go, rust, dotnet,
|
|
81
81
|
cpp, swift, flutter — and computes a readiness score with hard caps and visible provenance.
|
|
82
82
|
|
package/bin/uscha.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andresmassello/uscha",
|
|
3
|
-
"version": "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",
|
|
@@ -190,3 +190,21 @@ covered.
|
|
|
190
190
|
|
|
191
191
|
Never overwrite an existing approved golden. If a `.received` already exists, regenerate it;
|
|
192
192
|
if a `.approved` exists, it is the human's — leave it untouched and surface the diff.
|
|
193
|
+
|
|
194
|
+
## Record the coverage map (ADR-006)
|
|
195
|
+
|
|
196
|
+
After the `.received` is produced and while the harness is still the thing that just ran,
|
|
197
|
+
record WHICH source files it exercised — the mapping that lets the fast-path veto a change
|
|
198
|
+
touching code a golden froze:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
python qa_ledger.py golden-coverage --harness <harness> --golden <the .approved sibling>
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
This is **derived measurement, not judgment**: the agent may write `golden.coverage.json`.
|
|
205
|
+
INV-GOLDEN-01 governs the `.approved` bytes, which encode judgment, and nothing here changes
|
|
206
|
+
that — the human still approves the golden itself.
|
|
207
|
+
|
|
208
|
+
`coverage.py` is a capture-time dependency. Without it the command writes **nothing** and exits
|
|
209
|
+
2: an empty map would read as "this golden covers nothing", which is exactly the lie that would
|
|
210
|
+
let the veto pass. Skipping the map is honest; recording an empty one is not.
|
|
@@ -61,6 +61,7 @@ import re
|
|
|
61
61
|
import shutil
|
|
62
62
|
import subprocess
|
|
63
63
|
import sys
|
|
64
|
+
import tempfile
|
|
64
65
|
import unicodedata
|
|
65
66
|
import xml.etree.ElementTree as ET
|
|
66
67
|
from datetime import datetime, timezone
|
|
@@ -1857,6 +1858,58 @@ def cmd_init(args):
|
|
|
1857
1858
|
f"coverage_threshold={defaults.get('coverage_threshold')})")
|
|
1858
1859
|
|
|
1859
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
|
+
|
|
1860
1913
|
def _snapshot(ledger, name):
|
|
1861
1914
|
node = _repo_node(ledger, name)
|
|
1862
1915
|
cfg = _repo_cfg(ledger, name) if name != "integration" else {"path": ".", "type": "maven"}
|
|
@@ -1867,6 +1920,7 @@ def _snapshot(ledger, name):
|
|
|
1867
1920
|
"coverage": coverage(path, rtype),
|
|
1868
1921
|
"tests": test_count(path, rtype),
|
|
1869
1922
|
"loc": count_loc(path, rtype),
|
|
1923
|
+
"origin": _evidence_origin(path),
|
|
1870
1924
|
}
|
|
1871
1925
|
node["snapshots"].append(snap)
|
|
1872
1926
|
return snap
|
|
@@ -1886,6 +1940,7 @@ def cmd_snapshot(args):
|
|
|
1886
1940
|
f"coverage={cov['pct']}% (found={cov['report_found']}), "
|
|
1887
1941
|
f"tests={tests['total']} (found={tests['report_found']}), "
|
|
1888
1942
|
f"freshness={freshness.get('status', 'unknown')}, "
|
|
1943
|
+
f"origin={_origin_label(snap.get('origin'))}, "
|
|
1889
1944
|
f"prod_loc={loc['prod_loc']}, test_loc={loc['test_loc']}")
|
|
1890
1945
|
if freshness.get("status") == "stale":
|
|
1891
1946
|
print(f" test evidence stale: {freshness.get('reason')}")
|
|
@@ -2847,6 +2902,64 @@ def cmd_fastpath_eval(args):
|
|
|
2847
2902
|
sig("protected_paths", hits if hits else 0,
|
|
2848
2903
|
"no touched file matches a protected glob", "config globs over " + src,
|
|
2849
2904
|
not hits)
|
|
2905
|
+
# ADR-006: the golden-touched veto. OPT-IN -- absent flag, no signal at all
|
|
2906
|
+
# and behavior identical to 1.57.0+. DECLARED -- fail-closed: a missing or
|
|
2907
|
+
# empty mapping DENIES, because "could not measure" never grants a shortcut.
|
|
2908
|
+
if fp.get("forbid_when_golden_touched"):
|
|
2909
|
+
gcm = _load_golden_coverage(repo_path) # malformed -> exit 2, never silent
|
|
2910
|
+
gmap = (gcm or {}).get("goldens") or {}
|
|
2911
|
+
# Enumerate the goldens that actually EXIST. A manifest knowing only SOME
|
|
2912
|
+
# of them would otherwise assert "no touched file is covered by a golden"
|
|
2913
|
+
# about goldens it has never measured -- an ALLOW built on ignorance, which
|
|
2914
|
+
# is precisely the silent bypass this veto exists to prevent. Found by
|
|
2915
|
+
# fresh review and reproduced: 2 goldens in the tree, 1 in the map, a diff
|
|
2916
|
+
# touching the unmapped one's source -> ALLOW. Same glob shape cmd_golden_diff
|
|
2917
|
+
# already uses to locate goldens; no new mechanism.
|
|
2918
|
+
_sfx = ".appro" + "ved"
|
|
2919
|
+
_hits = set(glob.glob(os.path.join(repo_path, "**", "*" + _sfx),
|
|
2920
|
+
recursive=True))
|
|
2921
|
+
_hits |= set(glob.glob(os.path.join(repo_path, "**", "*" + _sfx + ".*"),
|
|
2922
|
+
recursive=True))
|
|
2923
|
+
_tree = set()
|
|
2924
|
+
for _p in _hits:
|
|
2925
|
+
if os.path.isfile(_p):
|
|
2926
|
+
_r = _gc_rel(os.path.abspath(_p), os.path.abspath(repo_path))
|
|
2927
|
+
if _r:
|
|
2928
|
+
_tree.add(_r)
|
|
2929
|
+
_unmapped = sorted(_tree - set(gmap))
|
|
2930
|
+
if _unmapped:
|
|
2931
|
+
# covers the manifest-absent case too: with goldens present and no
|
|
2932
|
+
# manifest, every one of them is unmapped.
|
|
2933
|
+
sig("golden_touched", _unmapped[:5],
|
|
2934
|
+
"every golden in the tree carries a measured map",
|
|
2935
|
+
GOLDEN_COVERAGE_FILE + " (missing or incomplete -- run "
|
|
2936
|
+
"golden-coverage for each golden)", False)
|
|
2937
|
+
elif not _tree:
|
|
2938
|
+
# No golden exists, so none can be touched. This is a MEASUREMENT
|
|
2939
|
+
# ("nothing to cover"), not an absence of one -- denying forever a
|
|
2940
|
+
# repo that has no goldens would be ceremony, not rigor.
|
|
2941
|
+
sig("golden_touched", 0, "no golden in the tree to be covered",
|
|
2942
|
+
"glob over " + repo_path, True)
|
|
2943
|
+
else:
|
|
2944
|
+
covered = {}
|
|
2945
|
+
_commits, _tools = set(), set()
|
|
2946
|
+
for _g, _e in gcm["goldens"].items():
|
|
2947
|
+
for _f in _e.get("files", []):
|
|
2948
|
+
covered.setdefault(_f, []).append(_g)
|
|
2949
|
+
if _e.get("captured_at_commit"):
|
|
2950
|
+
_commits.add(_e["captured_at_commit"][:8])
|
|
2951
|
+
if _e.get("tool"):
|
|
2952
|
+
_tools.add(_e["tool"])
|
|
2953
|
+
ghits = ["%s (golden: %s)" % (f, ", ".join(covered[f]))
|
|
2954
|
+
for f in files if f in covered]
|
|
2955
|
+
# provenance travels with the verdict (ADR-006: no freshness gate,
|
|
2956
|
+
# but every verdict says which capture it trusted)
|
|
2957
|
+
prov = "%s @ %s (%s)" % (
|
|
2958
|
+
GOLDEN_COVERAGE_FILE,
|
|
2959
|
+
",".join(sorted(_commits)) if _commits else "no commit recorded",
|
|
2960
|
+
", ".join(sorted(_tools)) if _tools else "no tool recorded")
|
|
2961
|
+
sig("golden_touched", ghits if ghits else 0,
|
|
2962
|
+
"no touched file is covered by a golden", prov, not ghits)
|
|
2850
2963
|
|
|
2851
2964
|
verdict = "ALLOW" if not deny else "DENY"
|
|
2852
2965
|
# Escalation means "an ACTIVE fast-path run outgrew its thresholds" -- so it gates on the
|
|
@@ -3072,6 +3185,158 @@ def cmd_spec_drift(args):
|
|
|
3072
3185
|
|
|
3073
3186
|
|
|
3074
3187
|
|
|
3188
|
+
# --------------------------------------------------------------------------- #
|
|
3189
|
+
# golden-coverage (ADR-006: the golden<->source mapping, DERIVED BY MEASUREMENT)
|
|
3190
|
+
# --------------------------------------------------------------------------- #
|
|
3191
|
+
|
|
3192
|
+
GOLDEN_COVERAGE_FILE = "golden.coverage.json"
|
|
3193
|
+
|
|
3194
|
+
|
|
3195
|
+
def _load_golden_coverage(root):
|
|
3196
|
+
"""Read the measured golden<->source manifest. Strict shape, mirroring
|
|
3197
|
+
_load_scrub_rules: a typo must NOT degrade into "no mapping" in silence, because
|
|
3198
|
+
under a declared veto that silence would GRANT the shortcut it exists to deny.
|
|
3199
|
+
Absent file -> None (the caller decides; with the veto declared, absent is DENY)."""
|
|
3200
|
+
path = os.path.join(root, GOLDEN_COVERAGE_FILE)
|
|
3201
|
+
if not os.path.isfile(path):
|
|
3202
|
+
return None
|
|
3203
|
+
try:
|
|
3204
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
3205
|
+
spec = json.load(fh)
|
|
3206
|
+
if not isinstance(spec, dict) or not isinstance(spec.get("goldens"), dict):
|
|
3207
|
+
raise TypeError('expected {"goldens": {"<golden path>": {"files": [...]}}}')
|
|
3208
|
+
for g, entry in spec["goldens"].items():
|
|
3209
|
+
if not isinstance(entry, dict) or not isinstance(entry.get("files"), list):
|
|
3210
|
+
raise TypeError("golden %r has no files list" % g)
|
|
3211
|
+
for f in entry["files"]:
|
|
3212
|
+
if not isinstance(f, str):
|
|
3213
|
+
raise TypeError("golden %r maps a non-string file" % g)
|
|
3214
|
+
return spec
|
|
3215
|
+
except (json.JSONDecodeError, TypeError, KeyError) as exc:
|
|
3216
|
+
print("[qa_ledger] %s invalid (%s) - the golden mapping is not skipped in "
|
|
3217
|
+
"silence: fix the file or delete it." % (path, exc), file=sys.stderr)
|
|
3218
|
+
sys.exit(2)
|
|
3219
|
+
|
|
3220
|
+
|
|
3221
|
+
def _gc_rel(path, root):
|
|
3222
|
+
# realpath BOTH sides before comparing. On Windows a temp dir under a username longer
|
|
3223
|
+
# than 8 chars is reported in 8.3 short form (RUNNER~1) by one side and long form by the
|
|
3224
|
+
# other; relpath then yields "../.." and a file INSIDE the repo is filtered out as
|
|
3225
|
+
# outside it -- silently shrinking the map. Invisible on a machine whose username does
|
|
3226
|
+
# not mangle (which is why local Windows was green and Windows CI was not).
|
|
3227
|
+
try:
|
|
3228
|
+
rel = os.path.relpath(os.path.realpath(path), os.path.realpath(root))
|
|
3229
|
+
except ValueError: # different drive on Windows -- outside the repo either way
|
|
3230
|
+
return None
|
|
3231
|
+
rel = rel.replace("\\", "/")
|
|
3232
|
+
return None if rel.startswith("../") else rel
|
|
3233
|
+
|
|
3234
|
+
|
|
3235
|
+
def cmd_golden_coverage(args):
|
|
3236
|
+
"""Record the MEASURED source files a golden's harness exercises (ADR-006).
|
|
3237
|
+
|
|
3238
|
+
The harnesses drive their subject through subprocess, so instrumenting only the parent
|
|
3239
|
+
measures nothing: coverage is injected into EVERY python the harness spawns via a
|
|
3240
|
+
sitecustomize on PYTHONPATH plus COVERAGE_PROCESS_START -- the documented multiprocess
|
|
3241
|
+
technique, and the same PYTHONPATH-injection shape this repo's fault tests already use.
|
|
3242
|
+
|
|
3243
|
+
coverage.py is an optional CAPTURE-time dependency (the engine stays stdlib-only at
|
|
3244
|
+
runtime). Absent, this writes NOTHING and exits 2: an empty map would read as
|
|
3245
|
+
"this golden covers nothing", which is the one lie that would let the veto pass."""
|
|
3246
|
+
try:
|
|
3247
|
+
import coverage
|
|
3248
|
+
except ImportError:
|
|
3249
|
+
print("[qa_ledger] coverage.py is not installed - refusing to write a map that was "
|
|
3250
|
+
"not measured (an empty map reads as 'covers nothing'). pip install coverage",
|
|
3251
|
+
file=sys.stderr)
|
|
3252
|
+
sys.exit(2)
|
|
3253
|
+
|
|
3254
|
+
root = os.path.abspath(args.dir or ".")
|
|
3255
|
+
harness = os.path.abspath(args.harness)
|
|
3256
|
+
if not os.path.isfile(harness):
|
|
3257
|
+
print("[qa_ledger] harness not found: %s" % harness, file=sys.stderr)
|
|
3258
|
+
sys.exit(2)
|
|
3259
|
+
|
|
3260
|
+
tmp = tempfile.mkdtemp(prefix="uscha-gc-")
|
|
3261
|
+
try:
|
|
3262
|
+
data_file = os.path.join(tmp, ".coverage")
|
|
3263
|
+
rc = os.path.join(tmp, "cov.rc")
|
|
3264
|
+
with open(rc, "w", encoding="utf-8") as fh:
|
|
3265
|
+
fh.write("[run]\nparallel = True\ndata_file = %s\n"
|
|
3266
|
+
% data_file.replace("\\", "/"))
|
|
3267
|
+
with open(os.path.join(tmp, "sitecustomize.py"), "w", encoding="utf-8") as fh:
|
|
3268
|
+
fh.write("import coverage\ncoverage.process_startup()\n")
|
|
3269
|
+
|
|
3270
|
+
env = dict(os.environ)
|
|
3271
|
+
env["COVERAGE_PROCESS_START"] = rc
|
|
3272
|
+
env["PYTHONPATH"] = tmp + os.pathsep + env.get("PYTHONPATH", "")
|
|
3273
|
+
env["PYTHONIOENCODING"] = "utf-8"
|
|
3274
|
+
r = subprocess.run([sys.executable, harness], cwd=root, env=env,
|
|
3275
|
+
capture_output=True, text=True, encoding="utf-8",
|
|
3276
|
+
errors="replace")
|
|
3277
|
+
if r.returncode != 0:
|
|
3278
|
+
print("[qa_ledger] the harness failed (exit %d) - no map recorded from a run "
|
|
3279
|
+
"that did not complete:\n%s" % (r.returncode, (r.stderr or "")[-1500:]),
|
|
3280
|
+
file=sys.stderr)
|
|
3281
|
+
sys.exit(2)
|
|
3282
|
+
|
|
3283
|
+
cov = coverage.Coverage(data_file=data_file)
|
|
3284
|
+
try:
|
|
3285
|
+
cov.combine()
|
|
3286
|
+
cov.save()
|
|
3287
|
+
except Exception as exc:
|
|
3288
|
+
# Never silent: a PARTIAL combine yields an incomplete-but-non-empty file list,
|
|
3289
|
+
# which slips past the empty-map guard below and records a map that under-reports
|
|
3290
|
+
# what the golden covers. The empty case still exits 2; this one is announced so a
|
|
3291
|
+
# human sees the map may be short (fresh-review finding).
|
|
3292
|
+
print("[qa_ledger] coverage combine reported: %s - the map below may be "
|
|
3293
|
+
"incomplete; re-run before trusting it." % exc, file=sys.stderr)
|
|
3294
|
+
measured = sorted(cov.get_data().measured_files())
|
|
3295
|
+
harness_rel = _gc_rel(harness, root)
|
|
3296
|
+
files = []
|
|
3297
|
+
for m in measured:
|
|
3298
|
+
rel = _gc_rel(os.path.abspath(m), root)
|
|
3299
|
+
# the harness measures the SUBJECT, not itself; sitecustomize is our scaffolding
|
|
3300
|
+
if not rel or rel == harness_rel or rel.endswith("/sitecustomize.py"):
|
|
3301
|
+
continue
|
|
3302
|
+
files.append(rel)
|
|
3303
|
+
files = sorted(set(files))
|
|
3304
|
+
finally:
|
|
3305
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
3306
|
+
|
|
3307
|
+
if not files:
|
|
3308
|
+
print("[qa_ledger] the run measured no source file inside %s - refusing to record "
|
|
3309
|
+
"an empty map (it would read as 'covers nothing')." % root, file=sys.stderr)
|
|
3310
|
+
sys.exit(2)
|
|
3311
|
+
|
|
3312
|
+
head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=root,
|
|
3313
|
+
capture_output=True, text=True)
|
|
3314
|
+
commit = head.stdout.strip() if head.returncode == 0 else None
|
|
3315
|
+
golden_rel = _gc_rel(os.path.abspath(args.golden), root) or args.golden
|
|
3316
|
+
|
|
3317
|
+
path = os.path.join(root, GOLDEN_COVERAGE_FILE)
|
|
3318
|
+
manifest = _load_golden_coverage(root) or {"goldens": {}}
|
|
3319
|
+
manifest["goldens"][golden_rel] = {
|
|
3320
|
+
"harness": harness_rel,
|
|
3321
|
+
"files": files,
|
|
3322
|
+
"captured_at": _now(),
|
|
3323
|
+
"captured_at_commit": commit,
|
|
3324
|
+
"tool": "coverage.py " + coverage.__version__,
|
|
3325
|
+
}
|
|
3326
|
+
with open(path, "w", encoding="utf-8", newline="\n") as fh:
|
|
3327
|
+
json.dump(manifest, fh, indent=2, ensure_ascii=False, sort_keys=True)
|
|
3328
|
+
fh.write("\n")
|
|
3329
|
+
|
|
3330
|
+
if args.json:
|
|
3331
|
+
print(json.dumps(manifest["goldens"][golden_rel], indent=2, ensure_ascii=False))
|
|
3332
|
+
else:
|
|
3333
|
+
print("GOLDEN-COVERAGE %s: %d source file(s) measured -> %s"
|
|
3334
|
+
% (golden_rel, len(files), GOLDEN_COVERAGE_FILE))
|
|
3335
|
+
for f in files[:20]:
|
|
3336
|
+
print(" " + f)
|
|
3337
|
+
|
|
3338
|
+
|
|
3339
|
+
|
|
3075
3340
|
def cmd_escalate(args):
|
|
3076
3341
|
ledger = _load(args.ledger)
|
|
3077
3342
|
_repo_node(ledger, args.repo)
|
|
@@ -4108,6 +4373,16 @@ def cmd_dashboard(args):
|
|
|
4108
4373
|
for r in {e.get("repo") for e in ledger["fast_path"]}}
|
|
4109
4374
|
if ledger.get("spec_drift"):
|
|
4110
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
|
|
4111
4386
|
if getattr(args, "json", False):
|
|
4112
4387
|
print(json.dumps(out, indent=2, ensure_ascii=False))
|
|
4113
4388
|
return
|
|
@@ -6812,6 +7087,14 @@ def build_parser():
|
|
|
6812
7087
|
pfp.add_argument("--json", action="store_true")
|
|
6813
7088
|
pfp.set_defaults(func=cmd_fastpath_eval)
|
|
6814
7089
|
|
|
7090
|
+
pgc = sub.add_parser("golden-coverage",
|
|
7091
|
+
help="record the MEASURED source files a golden's harness exercises (ADR-006)")
|
|
7092
|
+
pgc.add_argument("--harness", required=True, help="script that drives the subject")
|
|
7093
|
+
pgc.add_argument("--golden", required=True, help="the golden this map belongs to")
|
|
7094
|
+
pgc.add_argument("--dir", default=".", help="repo root holding " + GOLDEN_COVERAGE_FILE)
|
|
7095
|
+
pgc.add_argument("--json", action="store_true")
|
|
7096
|
+
pgc.set_defaults(func=cmd_golden_coverage)
|
|
7097
|
+
|
|
6815
7098
|
psd = sub.add_parser("spec-drift",
|
|
6816
7099
|
help="advisory spec-vs-code drift from git commit dates (ADR-005); never gates, exit 0 always")
|
|
6817
7100
|
psd.add_argument("--ledger", default="QA-LEDGER.json")
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
# mirador-watch.ps1 -- live second-screen mirador (uscha-kit 1.34.0), Windows.
|
|
2
|
-
# Regenerates mirador.html every N seconds from the current ledger; the page is rendered
|
|
3
|
-
# with a meta-refresh at the same interval, so a browser open on it updates on its own.
|
|
4
|
-
#
|
|
5
|
-
# Usage (run in a spare terminal, from the project root):
|
|
6
|
-
# powershell -NoProfile -File <kit>\.claude\skills\uscha-mirador\mirador-watch.ps1 [-Interval 30]
|
|
7
|
-
# then open mirador.html in a browser on your second screen. Ctrl-C to stop.
|
|
8
|
-
#
|
|
9
|
-
# Overridable via env: ENGINE, LEDGER, TEMPLATE, OUT, PYTHON.
|
|
10
|
-
param([int]$Interval = 30)
|
|
11
|
-
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
12
|
-
$engine = if ($env:ENGINE) { $env:ENGINE } else { Join-Path $here "..\uscha-devloop\qa_ledger.py" }
|
|
13
|
-
$ledger = if ($env:LEDGER) { $env:LEDGER } else { "QA-LEDGER.json" }
|
|
14
|
-
$template = if ($env:TEMPLATE) { $env:TEMPLATE } else { Join-Path $here "mirador.template.html" }
|
|
15
|
-
$out = if ($env:OUT) { $env:OUT } else { "mirador.html" }
|
|
16
|
-
$py = if ($env:PYTHON) { $env:PYTHON } else { "python" }
|
|
17
|
-
Write-Host "mirador-watch: regenerating $out every ${Interval}s (Ctrl-C to stop). Open $out in a browser."
|
|
18
|
-
while ($true) {
|
|
19
|
-
& $py (Join-Path $here "mirador-render.py") --engine $engine --ledger $ledger --template $template --out $out --refresh $Interval --no-open
|
|
20
|
-
if ($LASTEXITCODE -ne 0) { Write-Host "mirador-watch: render failed (ledger missing? run uscha-devloop first) -- retrying" }
|
|
21
|
-
Start-Sleep -Seconds $Interval
|
|
22
|
-
}
|
|
1
|
+
# mirador-watch.ps1 -- live second-screen mirador (uscha-kit 1.34.0), Windows.
|
|
2
|
+
# Regenerates mirador.html every N seconds from the current ledger; the page is rendered
|
|
3
|
+
# with a meta-refresh at the same interval, so a browser open on it updates on its own.
|
|
4
|
+
#
|
|
5
|
+
# Usage (run in a spare terminal, from the project root):
|
|
6
|
+
# powershell -NoProfile -File <kit>\.claude\skills\uscha-mirador\mirador-watch.ps1 [-Interval 30]
|
|
7
|
+
# then open mirador.html in a browser on your second screen. Ctrl-C to stop.
|
|
8
|
+
#
|
|
9
|
+
# Overridable via env: ENGINE, LEDGER, TEMPLATE, OUT, PYTHON.
|
|
10
|
+
param([int]$Interval = 30)
|
|
11
|
+
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
12
|
+
$engine = if ($env:ENGINE) { $env:ENGINE } else { Join-Path $here "..\uscha-devloop\qa_ledger.py" }
|
|
13
|
+
$ledger = if ($env:LEDGER) { $env:LEDGER } else { "QA-LEDGER.json" }
|
|
14
|
+
$template = if ($env:TEMPLATE) { $env:TEMPLATE } else { Join-Path $here "mirador.template.html" }
|
|
15
|
+
$out = if ($env:OUT) { $env:OUT } else { "mirador.html" }
|
|
16
|
+
$py = if ($env:PYTHON) { $env:PYTHON } else { "python" }
|
|
17
|
+
Write-Host "mirador-watch: regenerating $out every ${Interval}s (Ctrl-C to stop). Open $out in a browser."
|
|
18
|
+
while ($true) {
|
|
19
|
+
& $py (Join-Path $here "mirador-render.py") --engine $engine --ledger $ledger --template $template --out $out --refresh $Interval --no-open
|
|
20
|
+
if ($LASTEXITCODE -ne 0) { Write-Host "mirador-watch: render failed (ledger missing? run uscha-devloop first) -- retrying" }
|
|
21
|
+
Start-Sleep -Seconds $Interval
|
|
22
|
+
}
|
|
File without changes
|
|
@@ -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,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "uscha",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.61.0",
|
|
5
5
|
"displayName": "Uscha",
|
|
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,
|
|
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": {
|
|
8
8
|
"name": "Andres Massello",
|
|
9
9
|
"url": "https://github.com/andresmassello"
|
package/uscha-kit/INSTALL.md
CHANGED
|
@@ -1,128 +1,128 @@
|
|
|
1
|
-
# Install Uscha
|
|
2
|
-
|
|
3
|
-
Uscha installs as a machine-level helper for coding agents. The recommended path
|
|
4
|
-
is npm/npx because it works the same on a fresh Codex or Claude Code machine.
|
|
5
|
-
|
|
6
|
-
The method itself — the paradigm, the five rules, the skills and the library — lives at
|
|
7
|
-
**[uscha.dev](https://uscha.dev)**.
|
|
8
|
-
|
|
9
|
-
## Quick path
|
|
10
|
-
|
|
11
|
-
### Codex Desktop
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
npx --yes @andresmassello/uscha@latest version
|
|
15
|
-
npx --yes @andresmassello/uscha@latest install --target codex --dry-run
|
|
16
|
-
npx --yes @andresmassello/uscha@latest install --target codex
|
|
17
|
-
npx --yes @andresmassello/uscha@latest doctor --target codex
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
Then restart Codex or open a new thread. The installer registers Uscha as a
|
|
21
|
-
personal local plugin under `~/plugins/uscha` and updates
|
|
22
|
-
`~/.agents/plugins/marketplace.json`. It preflights that marketplace before replacing the plugin tree and writes the install marker last.
|
|
23
|
-
|
|
24
|
-
### Claude Code
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
npx --yes @andresmassello/uscha@latest install --target claude --dry-run
|
|
28
|
-
npx --yes @andresmassello/uscha@latest install --target claude
|
|
29
|
-
npx --yes @andresmassello/uscha@latest doctor --target claude
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
This installs the `uscha-*` skills and registers a portable Python `PreToolUse` hook under `~/.claude` while preserving unrelated `settings.json` entries. Restart or reload Claude Code after installing.
|
|
33
|
-
|
|
34
|
-
### Same machine uses both
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
npx --yes @andresmassello/uscha@latest install --target both --dry-run
|
|
38
|
-
npx --yes @andresmassello/uscha@latest install --target both
|
|
39
|
-
npx --yes @andresmassello/uscha@latest doctor --target both
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
## Prepare a project repo
|
|
43
|
-
|
|
44
|
-
After the machine install, initialize each project where Uscha should govern the
|
|
45
|
-
workflow:
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
npx --yes @andresmassello/uscha@latest init --repo . --dry-run
|
|
49
|
-
npx --yes @andresmassello/uscha@latest init --repo .
|
|
50
|
-
# Existing differing files are preserved; use --force only to replace them deliberately.
|
|
51
|
-
npx --yes @andresmassello/uscha@latest init --repo . --force
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
`init` exits nonzero and reports conflicts for differing `uscha.config.json`, `CLAUDE.md`, `CONSTITUTION.md`, or `.gitattributes`; `--dry-run` performs the same conflict check without writing.
|
|
55
|
-
|
|
56
|
-
`init` also installs the **progress statusline** (kit 1.46.0): it copies
|
|
57
|
-
`.claude/scripts/uscha_{statusline,progress}.py` and merges a `statusLine` + a `Stop` hook into
|
|
58
|
-
`.claude/settings.json` (never clobbering an existing `statusLine`). Add a `label`, `roadmap`
|
|
59
|
-
and `build_priority` to your `repos[]` entry to drive it; with no data it stays hidden.
|
|
60
|
-
|
|
61
|
-
Project state stays in the project: `uscha.config.json`, `QA-LEDGER.json`,
|
|
62
|
-
`ACCEPTANCE.md`, and approved golden fixtures when used.
|
|
63
|
-
|
|
64
|
-
## See the dashboard (mirador)
|
|
65
|
-
|
|
66
|
-
From the root of any project that has a `QA-LEDGER.json`, one command renders the mirador and
|
|
67
|
-
opens it — no python, no paths:
|
|
68
|
-
|
|
69
|
-
```bash
|
|
70
|
-
npx --yes @andresmassello/uscha@latest mirador # one glance: render + open
|
|
71
|
-
npx --yes @andresmassello/uscha@latest mirador --watch # live second-screen view (auto-refresh)
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
It defaults to the `QA-LEDGER.json` convention in the current directory (pass `--ledger` to
|
|
75
|
-
point elsewhere) and prints the absolute path it wrote. `--watch` re-renders every `--interval`
|
|
76
|
-
seconds (default 30) into one self-reloading tab.
|
|
77
|
-
|
|
78
|
-
## Requirements
|
|
79
|
-
|
|
80
|
-
| Requirement | Why |
|
|
81
|
-
|-------------|-----|
|
|
82
|
-
| Node.js + npm | Runs the universal `npx` entrypoint. |
|
|
83
|
-
| Python 3.8+ | Runs the canonical stdlib installer and engine. |
|
|
84
|
-
| Git | Used by the method and by project setup checks. |
|
|
85
|
-
| Codex Desktop and/or Claude Code | The agent runtime you want to install Uscha into. |
|
|
86
|
-
|
|
87
|
-
No `pip install` is required. The engine is Python stdlib-only.
|
|
88
|
-
|
|
89
|
-
## Other install options
|
|
90
|
-
|
|
91
|
-
| Option | Use when | Tradeoff |
|
|
92
|
-
|--------|----------|----------|
|
|
93
|
-
| `npx @andresmassello/uscha@latest ...` | Normal install/update on any machine. | Requires npm registry access. |
|
|
94
|
-
| Git checkout + `python uscha-kit/install-uscha.py ...` | Developing Uscha itself or testing unreleased changes. | You must clone/pull the repo yourself. |
|
|
95
|
-
| `--mode link` from a checkout | This machine develops the kit and installed skills should follow local edits. | Links are great for development, risky for normal users. |
|
|
96
|
-
| Claude Code plugin commands | You specifically want Claude Code's native plugin flow. | Codex still needs the npm/git installer path. |
|
|
97
|
-
| Manual copy | Debugging the installer. | Easy to drift; not recommended for adoption. |
|
|
98
|
-
|
|
99
|
-
Development checkout example:
|
|
100
|
-
|
|
101
|
-
```bash
|
|
102
|
-
git clone https://github.com/andresmassello/uscha.git
|
|
103
|
-
cd uscha
|
|
104
|
-
python uscha-kit/install-uscha.py install --target both --mode link --dry-run
|
|
105
|
-
python uscha-kit/install-uscha.py install --target both --mode link
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
Claude Code plugin option:
|
|
109
|
-
|
|
110
|
-
```text
|
|
111
|
-
/plugin marketplace add andresmassello/uscha
|
|
112
|
-
/plugin install uscha@uscha
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
## Update and verify
|
|
116
|
-
|
|
117
|
-
```bash
|
|
118
|
-
npm view @andresmassello/uscha version
|
|
119
|
-
npx --yes @andresmassello/uscha@latest version
|
|
120
|
-
npx --yes @andresmassello/uscha@latest install --target both
|
|
121
|
-
npx --yes @andresmassello/uscha@latest doctor --target both
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
`doctor` exits 1 for any unhealthy target in either text or `--json` mode. It checks installed skill presence, manifest/marketplace or hook registration, marker, and version; it does not measure file-content integrity.
|
|
125
|
-
|
|
126
|
-
If `npm view` returns `404` immediately after a new release, wait a few minutes:
|
|
127
|
-
npm search/dist-tags can propagate before the package metadata endpoint used by
|
|
128
|
-
`npx`. Do not republish the same version while propagation is in progress.
|
|
1
|
+
# Install Uscha
|
|
2
|
+
|
|
3
|
+
Uscha installs as a machine-level helper for coding agents. The recommended path
|
|
4
|
+
is npm/npx because it works the same on a fresh Codex or Claude Code machine.
|
|
5
|
+
|
|
6
|
+
The method itself — the paradigm, the five rules, the skills and the library — lives at
|
|
7
|
+
**[uscha.dev](https://uscha.dev)**.
|
|
8
|
+
|
|
9
|
+
## Quick path
|
|
10
|
+
|
|
11
|
+
### Codex Desktop
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx --yes @andresmassello/uscha@latest version
|
|
15
|
+
npx --yes @andresmassello/uscha@latest install --target codex --dry-run
|
|
16
|
+
npx --yes @andresmassello/uscha@latest install --target codex
|
|
17
|
+
npx --yes @andresmassello/uscha@latest doctor --target codex
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Then restart Codex or open a new thread. The installer registers Uscha as a
|
|
21
|
+
personal local plugin under `~/plugins/uscha` and updates
|
|
22
|
+
`~/.agents/plugins/marketplace.json`. It preflights that marketplace before replacing the plugin tree and writes the install marker last.
|
|
23
|
+
|
|
24
|
+
### Claude Code
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx --yes @andresmassello/uscha@latest install --target claude --dry-run
|
|
28
|
+
npx --yes @andresmassello/uscha@latest install --target claude
|
|
29
|
+
npx --yes @andresmassello/uscha@latest doctor --target claude
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
This installs the `uscha-*` skills and registers a portable Python `PreToolUse` hook under `~/.claude` while preserving unrelated `settings.json` entries. Restart or reload Claude Code after installing.
|
|
33
|
+
|
|
34
|
+
### Same machine uses both
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx --yes @andresmassello/uscha@latest install --target both --dry-run
|
|
38
|
+
npx --yes @andresmassello/uscha@latest install --target both
|
|
39
|
+
npx --yes @andresmassello/uscha@latest doctor --target both
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Prepare a project repo
|
|
43
|
+
|
|
44
|
+
After the machine install, initialize each project where Uscha should govern the
|
|
45
|
+
workflow:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx --yes @andresmassello/uscha@latest init --repo . --dry-run
|
|
49
|
+
npx --yes @andresmassello/uscha@latest init --repo .
|
|
50
|
+
# Existing differing files are preserved; use --force only to replace them deliberately.
|
|
51
|
+
npx --yes @andresmassello/uscha@latest init --repo . --force
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`init` exits nonzero and reports conflicts for differing `uscha.config.json`, `CLAUDE.md`, `CONSTITUTION.md`, or `.gitattributes`; `--dry-run` performs the same conflict check without writing.
|
|
55
|
+
|
|
56
|
+
`init` also installs the **progress statusline** (kit 1.46.0): it copies
|
|
57
|
+
`.claude/scripts/uscha_{statusline,progress}.py` and merges a `statusLine` + a `Stop` hook into
|
|
58
|
+
`.claude/settings.json` (never clobbering an existing `statusLine`). Add a `label`, `roadmap`
|
|
59
|
+
and `build_priority` to your `repos[]` entry to drive it; with no data it stays hidden.
|
|
60
|
+
|
|
61
|
+
Project state stays in the project: `uscha.config.json`, `QA-LEDGER.json`,
|
|
62
|
+
`ACCEPTANCE.md`, and approved golden fixtures when used.
|
|
63
|
+
|
|
64
|
+
## See the dashboard (mirador)
|
|
65
|
+
|
|
66
|
+
From the root of any project that has a `QA-LEDGER.json`, one command renders the mirador and
|
|
67
|
+
opens it — no python, no paths:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npx --yes @andresmassello/uscha@latest mirador # one glance: render + open
|
|
71
|
+
npx --yes @andresmassello/uscha@latest mirador --watch # live second-screen view (auto-refresh)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
It defaults to the `QA-LEDGER.json` convention in the current directory (pass `--ledger` to
|
|
75
|
+
point elsewhere) and prints the absolute path it wrote. `--watch` re-renders every `--interval`
|
|
76
|
+
seconds (default 30) into one self-reloading tab.
|
|
77
|
+
|
|
78
|
+
## Requirements
|
|
79
|
+
|
|
80
|
+
| Requirement | Why |
|
|
81
|
+
|-------------|-----|
|
|
82
|
+
| Node.js + npm | Runs the universal `npx` entrypoint. |
|
|
83
|
+
| Python 3.8+ | Runs the canonical stdlib installer and engine. |
|
|
84
|
+
| Git | Used by the method and by project setup checks. |
|
|
85
|
+
| Codex Desktop and/or Claude Code | The agent runtime you want to install Uscha into. |
|
|
86
|
+
|
|
87
|
+
No `pip install` is required. The engine is Python stdlib-only.
|
|
88
|
+
|
|
89
|
+
## Other install options
|
|
90
|
+
|
|
91
|
+
| Option | Use when | Tradeoff |
|
|
92
|
+
|--------|----------|----------|
|
|
93
|
+
| `npx @andresmassello/uscha@latest ...` | Normal install/update on any machine. | Requires npm registry access. |
|
|
94
|
+
| Git checkout + `python uscha-kit/install-uscha.py ...` | Developing Uscha itself or testing unreleased changes. | You must clone/pull the repo yourself. |
|
|
95
|
+
| `--mode link` from a checkout | This machine develops the kit and installed skills should follow local edits. | Links are great for development, risky for normal users. |
|
|
96
|
+
| Claude Code plugin commands | You specifically want Claude Code's native plugin flow. | Codex still needs the npm/git installer path. |
|
|
97
|
+
| Manual copy | Debugging the installer. | Easy to drift; not recommended for adoption. |
|
|
98
|
+
|
|
99
|
+
Development checkout example:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
git clone https://github.com/andresmassello/uscha.git
|
|
103
|
+
cd uscha
|
|
104
|
+
python uscha-kit/install-uscha.py install --target both --mode link --dry-run
|
|
105
|
+
python uscha-kit/install-uscha.py install --target both --mode link
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Claude Code plugin option:
|
|
109
|
+
|
|
110
|
+
```text
|
|
111
|
+
/plugin marketplace add andresmassello/uscha
|
|
112
|
+
/plugin install uscha@uscha
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Update and verify
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
npm view @andresmassello/uscha version
|
|
119
|
+
npx --yes @andresmassello/uscha@latest version
|
|
120
|
+
npx --yes @andresmassello/uscha@latest install --target both
|
|
121
|
+
npx --yes @andresmassello/uscha@latest doctor --target both
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`doctor` exits 1 for any unhealthy target in either text or `--json` mode. It checks installed skill presence, manifest/marketplace or hook registration, marker, and version; it does not measure file-content integrity.
|
|
125
|
+
|
|
126
|
+
If `npm view` returns `404` immediately after a new release, wait a few minutes:
|
|
127
|
+
npm search/dist-tags can propagate before the package metadata endpoint used by
|
|
128
|
+
`npx`. Do not republish the same version while propagation is in progress.
|
package/uscha-kit/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# uscha-kit
|
|
2
2
|
|
|
3
|
-
**Kit version:** v1.
|
|
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`,
|
|
@@ -62,6 +62,42 @@ and readiness is capped until a human runs `resolve-escalation`, after producing
|
|
|
62
62
|
ACCEPTANCE the change turned out to deserve. **The override is asymmetric (INV-RIGOR-02):**
|
|
63
63
|
you can always force the full path; nothing can force `ALLOW` over a measured `DENY`.
|
|
64
64
|
|
|
65
|
+
## Golden coverage (ADR-006) — the veto knows what a golden actually covers
|
|
66
|
+
|
|
67
|
+
A golden freezes behavior. Changing code a golden exercises is never a trivial change — but
|
|
68
|
+
until the engine knew WHICH source files a golden covers, that veto could not be measured, so
|
|
69
|
+
ADR-004 deferred it rather than ship a gate that pretended. `golden-coverage` builds the
|
|
70
|
+
mapping **by measurement**:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
python qa_ledger.py golden-coverage --harness tests/golden/harness-x.py --golden tests/golden/x.approved.json
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
It runs the harness under `coverage.py` and records the source files that actually executed
|
|
77
|
+
into `golden.coverage.json` at the repo root (same convention as `golden.scrub.json`). The
|
|
78
|
+
harnesses drive their subject through a **subprocess**, so instrumentation is injected into
|
|
79
|
+
every python they spawn — measuring only the parent would record nothing and produce an empty
|
|
80
|
+
map, which would read as "this golden covers nothing".
|
|
81
|
+
|
|
82
|
+
Then, **opt-in**, the fast-path grows a `golden_touched` signal:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
"fast_path": { "forbid_when_golden_touched": true }
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Absent or `false` → the veto does not exist and behavior is identical to earlier releases.
|
|
89
|
+
Declared → **fail-closed**: touching a mapped file denies (naming the golden and the file), and
|
|
90
|
+
so does a missing manifest, because "could not measure" never grants the shortcut. A malformed
|
|
91
|
+
manifest exits 2 rather than degrading into a silent "no mapping". Every verdict carries the
|
|
92
|
+
capture commit and tool version in the signal's `source` — provenance, not a freshness gate:
|
|
93
|
+
an aged map's real risk is a false negative, which cannot be detected without re-capturing.
|
|
94
|
+
|
|
95
|
+
Two honest limits (ADR-006): `coverage.py` is Python-only, so a harness in another language
|
|
96
|
+
cannot produce a map — declaring the veto there yields a permanent `DENY`, and the remedy is
|
|
97
|
+
not to declare it. And **file** granularity over-fires on monolithic files: in a repo whose
|
|
98
|
+
engine is one large module, the veto fires on nearly every change to it. When it errs, it errs
|
|
99
|
+
toward more ceremony, never less.
|
|
100
|
+
|
|
65
101
|
## Spec-drift (ADR-005) — the spec maintenance tax, made visible
|
|
66
102
|
|
|
67
103
|
Specs rot silently: the code moves and `SPEC.md` stays where it was. `spec-drift` detects
|
|
@@ -91,6 +127,27 @@ spec has no commit date to compare. The latest run lands in the ledger (`spec_dr
|
|
|
91
127
|
mirador can surface it. No readiness impact, no exit-code gate: a stale spec is a prompt for
|
|
92
128
|
a human conversation, not a blocked pipeline.
|
|
93
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
|
+
|
|
94
151
|
## End-to-end flow
|
|
95
152
|
|
|
96
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.
|
|
1
|
+
uscha-kit 1.61.0
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"AC-GM-01": true, "AC-GM-03": true, "AC-GM-05": true, "AC-GM-04": true, "AC-GM-02": true, "AC-GM-06": true, "AC-GM-07": true, "AC-GM-08": null}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"AC-EP-01": true, "AC-EP-02": true, "AC-EP-03": true, "AC-EP-05": true, "AC-EP-04": true}
|
|
@@ -190,3 +190,21 @@ covered.
|
|
|
190
190
|
|
|
191
191
|
Never overwrite an existing approved golden. If a `.received` already exists, regenerate it;
|
|
192
192
|
if a `.approved` exists, it is the human's — leave it untouched and surface the diff.
|
|
193
|
+
|
|
194
|
+
## Record the coverage map (ADR-006)
|
|
195
|
+
|
|
196
|
+
After the `.received` is produced and while the harness is still the thing that just ran,
|
|
197
|
+
record WHICH source files it exercised — the mapping that lets the fast-path veto a change
|
|
198
|
+
touching code a golden froze:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
python qa_ledger.py golden-coverage --harness <harness> --golden <the .approved sibling>
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
This is **derived measurement, not judgment**: the agent may write `golden.coverage.json`.
|
|
205
|
+
INV-GOLDEN-01 governs the `.approved` bytes, which encode judgment, and nothing here changes
|
|
206
|
+
that — the human still approves the golden itself.
|
|
207
|
+
|
|
208
|
+
`coverage.py` is a capture-time dependency. Without it the command writes **nothing** and exits
|
|
209
|
+
2: an empty map would read as "this golden covers nothing", which is exactly the lie that would
|
|
210
|
+
let the veto pass. Skipping the map is honest; recording an empty one is not.
|
|
@@ -61,6 +61,7 @@ import re
|
|
|
61
61
|
import shutil
|
|
62
62
|
import subprocess
|
|
63
63
|
import sys
|
|
64
|
+
import tempfile
|
|
64
65
|
import unicodedata
|
|
65
66
|
import xml.etree.ElementTree as ET
|
|
66
67
|
from datetime import datetime, timezone
|
|
@@ -1857,6 +1858,58 @@ def cmd_init(args):
|
|
|
1857
1858
|
f"coverage_threshold={defaults.get('coverage_threshold')})")
|
|
1858
1859
|
|
|
1859
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
|
+
|
|
1860
1913
|
def _snapshot(ledger, name):
|
|
1861
1914
|
node = _repo_node(ledger, name)
|
|
1862
1915
|
cfg = _repo_cfg(ledger, name) if name != "integration" else {"path": ".", "type": "maven"}
|
|
@@ -1867,6 +1920,7 @@ def _snapshot(ledger, name):
|
|
|
1867
1920
|
"coverage": coverage(path, rtype),
|
|
1868
1921
|
"tests": test_count(path, rtype),
|
|
1869
1922
|
"loc": count_loc(path, rtype),
|
|
1923
|
+
"origin": _evidence_origin(path),
|
|
1870
1924
|
}
|
|
1871
1925
|
node["snapshots"].append(snap)
|
|
1872
1926
|
return snap
|
|
@@ -1886,6 +1940,7 @@ def cmd_snapshot(args):
|
|
|
1886
1940
|
f"coverage={cov['pct']}% (found={cov['report_found']}), "
|
|
1887
1941
|
f"tests={tests['total']} (found={tests['report_found']}), "
|
|
1888
1942
|
f"freshness={freshness.get('status', 'unknown')}, "
|
|
1943
|
+
f"origin={_origin_label(snap.get('origin'))}, "
|
|
1889
1944
|
f"prod_loc={loc['prod_loc']}, test_loc={loc['test_loc']}")
|
|
1890
1945
|
if freshness.get("status") == "stale":
|
|
1891
1946
|
print(f" test evidence stale: {freshness.get('reason')}")
|
|
@@ -2847,6 +2902,64 @@ def cmd_fastpath_eval(args):
|
|
|
2847
2902
|
sig("protected_paths", hits if hits else 0,
|
|
2848
2903
|
"no touched file matches a protected glob", "config globs over " + src,
|
|
2849
2904
|
not hits)
|
|
2905
|
+
# ADR-006: the golden-touched veto. OPT-IN -- absent flag, no signal at all
|
|
2906
|
+
# and behavior identical to 1.57.0+. DECLARED -- fail-closed: a missing or
|
|
2907
|
+
# empty mapping DENIES, because "could not measure" never grants a shortcut.
|
|
2908
|
+
if fp.get("forbid_when_golden_touched"):
|
|
2909
|
+
gcm = _load_golden_coverage(repo_path) # malformed -> exit 2, never silent
|
|
2910
|
+
gmap = (gcm or {}).get("goldens") or {}
|
|
2911
|
+
# Enumerate the goldens that actually EXIST. A manifest knowing only SOME
|
|
2912
|
+
# of them would otherwise assert "no touched file is covered by a golden"
|
|
2913
|
+
# about goldens it has never measured -- an ALLOW built on ignorance, which
|
|
2914
|
+
# is precisely the silent bypass this veto exists to prevent. Found by
|
|
2915
|
+
# fresh review and reproduced: 2 goldens in the tree, 1 in the map, a diff
|
|
2916
|
+
# touching the unmapped one's source -> ALLOW. Same glob shape cmd_golden_diff
|
|
2917
|
+
# already uses to locate goldens; no new mechanism.
|
|
2918
|
+
_sfx = ".appro" + "ved"
|
|
2919
|
+
_hits = set(glob.glob(os.path.join(repo_path, "**", "*" + _sfx),
|
|
2920
|
+
recursive=True))
|
|
2921
|
+
_hits |= set(glob.glob(os.path.join(repo_path, "**", "*" + _sfx + ".*"),
|
|
2922
|
+
recursive=True))
|
|
2923
|
+
_tree = set()
|
|
2924
|
+
for _p in _hits:
|
|
2925
|
+
if os.path.isfile(_p):
|
|
2926
|
+
_r = _gc_rel(os.path.abspath(_p), os.path.abspath(repo_path))
|
|
2927
|
+
if _r:
|
|
2928
|
+
_tree.add(_r)
|
|
2929
|
+
_unmapped = sorted(_tree - set(gmap))
|
|
2930
|
+
if _unmapped:
|
|
2931
|
+
# covers the manifest-absent case too: with goldens present and no
|
|
2932
|
+
# manifest, every one of them is unmapped.
|
|
2933
|
+
sig("golden_touched", _unmapped[:5],
|
|
2934
|
+
"every golden in the tree carries a measured map",
|
|
2935
|
+
GOLDEN_COVERAGE_FILE + " (missing or incomplete -- run "
|
|
2936
|
+
"golden-coverage for each golden)", False)
|
|
2937
|
+
elif not _tree:
|
|
2938
|
+
# No golden exists, so none can be touched. This is a MEASUREMENT
|
|
2939
|
+
# ("nothing to cover"), not an absence of one -- denying forever a
|
|
2940
|
+
# repo that has no goldens would be ceremony, not rigor.
|
|
2941
|
+
sig("golden_touched", 0, "no golden in the tree to be covered",
|
|
2942
|
+
"glob over " + repo_path, True)
|
|
2943
|
+
else:
|
|
2944
|
+
covered = {}
|
|
2945
|
+
_commits, _tools = set(), set()
|
|
2946
|
+
for _g, _e in gcm["goldens"].items():
|
|
2947
|
+
for _f in _e.get("files", []):
|
|
2948
|
+
covered.setdefault(_f, []).append(_g)
|
|
2949
|
+
if _e.get("captured_at_commit"):
|
|
2950
|
+
_commits.add(_e["captured_at_commit"][:8])
|
|
2951
|
+
if _e.get("tool"):
|
|
2952
|
+
_tools.add(_e["tool"])
|
|
2953
|
+
ghits = ["%s (golden: %s)" % (f, ", ".join(covered[f]))
|
|
2954
|
+
for f in files if f in covered]
|
|
2955
|
+
# provenance travels with the verdict (ADR-006: no freshness gate,
|
|
2956
|
+
# but every verdict says which capture it trusted)
|
|
2957
|
+
prov = "%s @ %s (%s)" % (
|
|
2958
|
+
GOLDEN_COVERAGE_FILE,
|
|
2959
|
+
",".join(sorted(_commits)) if _commits else "no commit recorded",
|
|
2960
|
+
", ".join(sorted(_tools)) if _tools else "no tool recorded")
|
|
2961
|
+
sig("golden_touched", ghits if ghits else 0,
|
|
2962
|
+
"no touched file is covered by a golden", prov, not ghits)
|
|
2850
2963
|
|
|
2851
2964
|
verdict = "ALLOW" if not deny else "DENY"
|
|
2852
2965
|
# Escalation means "an ACTIVE fast-path run outgrew its thresholds" -- so it gates on the
|
|
@@ -3072,6 +3185,158 @@ def cmd_spec_drift(args):
|
|
|
3072
3185
|
|
|
3073
3186
|
|
|
3074
3187
|
|
|
3188
|
+
# --------------------------------------------------------------------------- #
|
|
3189
|
+
# golden-coverage (ADR-006: the golden<->source mapping, DERIVED BY MEASUREMENT)
|
|
3190
|
+
# --------------------------------------------------------------------------- #
|
|
3191
|
+
|
|
3192
|
+
GOLDEN_COVERAGE_FILE = "golden.coverage.json"
|
|
3193
|
+
|
|
3194
|
+
|
|
3195
|
+
def _load_golden_coverage(root):
|
|
3196
|
+
"""Read the measured golden<->source manifest. Strict shape, mirroring
|
|
3197
|
+
_load_scrub_rules: a typo must NOT degrade into "no mapping" in silence, because
|
|
3198
|
+
under a declared veto that silence would GRANT the shortcut it exists to deny.
|
|
3199
|
+
Absent file -> None (the caller decides; with the veto declared, absent is DENY)."""
|
|
3200
|
+
path = os.path.join(root, GOLDEN_COVERAGE_FILE)
|
|
3201
|
+
if not os.path.isfile(path):
|
|
3202
|
+
return None
|
|
3203
|
+
try:
|
|
3204
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
3205
|
+
spec = json.load(fh)
|
|
3206
|
+
if not isinstance(spec, dict) or not isinstance(spec.get("goldens"), dict):
|
|
3207
|
+
raise TypeError('expected {"goldens": {"<golden path>": {"files": [...]}}}')
|
|
3208
|
+
for g, entry in spec["goldens"].items():
|
|
3209
|
+
if not isinstance(entry, dict) or not isinstance(entry.get("files"), list):
|
|
3210
|
+
raise TypeError("golden %r has no files list" % g)
|
|
3211
|
+
for f in entry["files"]:
|
|
3212
|
+
if not isinstance(f, str):
|
|
3213
|
+
raise TypeError("golden %r maps a non-string file" % g)
|
|
3214
|
+
return spec
|
|
3215
|
+
except (json.JSONDecodeError, TypeError, KeyError) as exc:
|
|
3216
|
+
print("[qa_ledger] %s invalid (%s) - the golden mapping is not skipped in "
|
|
3217
|
+
"silence: fix the file or delete it." % (path, exc), file=sys.stderr)
|
|
3218
|
+
sys.exit(2)
|
|
3219
|
+
|
|
3220
|
+
|
|
3221
|
+
def _gc_rel(path, root):
|
|
3222
|
+
# realpath BOTH sides before comparing. On Windows a temp dir under a username longer
|
|
3223
|
+
# than 8 chars is reported in 8.3 short form (RUNNER~1) by one side and long form by the
|
|
3224
|
+
# other; relpath then yields "../.." and a file INSIDE the repo is filtered out as
|
|
3225
|
+
# outside it -- silently shrinking the map. Invisible on a machine whose username does
|
|
3226
|
+
# not mangle (which is why local Windows was green and Windows CI was not).
|
|
3227
|
+
try:
|
|
3228
|
+
rel = os.path.relpath(os.path.realpath(path), os.path.realpath(root))
|
|
3229
|
+
except ValueError: # different drive on Windows -- outside the repo either way
|
|
3230
|
+
return None
|
|
3231
|
+
rel = rel.replace("\\", "/")
|
|
3232
|
+
return None if rel.startswith("../") else rel
|
|
3233
|
+
|
|
3234
|
+
|
|
3235
|
+
def cmd_golden_coverage(args):
|
|
3236
|
+
"""Record the MEASURED source files a golden's harness exercises (ADR-006).
|
|
3237
|
+
|
|
3238
|
+
The harnesses drive their subject through subprocess, so instrumenting only the parent
|
|
3239
|
+
measures nothing: coverage is injected into EVERY python the harness spawns via a
|
|
3240
|
+
sitecustomize on PYTHONPATH plus COVERAGE_PROCESS_START -- the documented multiprocess
|
|
3241
|
+
technique, and the same PYTHONPATH-injection shape this repo's fault tests already use.
|
|
3242
|
+
|
|
3243
|
+
coverage.py is an optional CAPTURE-time dependency (the engine stays stdlib-only at
|
|
3244
|
+
runtime). Absent, this writes NOTHING and exits 2: an empty map would read as
|
|
3245
|
+
"this golden covers nothing", which is the one lie that would let the veto pass."""
|
|
3246
|
+
try:
|
|
3247
|
+
import coverage
|
|
3248
|
+
except ImportError:
|
|
3249
|
+
print("[qa_ledger] coverage.py is not installed - refusing to write a map that was "
|
|
3250
|
+
"not measured (an empty map reads as 'covers nothing'). pip install coverage",
|
|
3251
|
+
file=sys.stderr)
|
|
3252
|
+
sys.exit(2)
|
|
3253
|
+
|
|
3254
|
+
root = os.path.abspath(args.dir or ".")
|
|
3255
|
+
harness = os.path.abspath(args.harness)
|
|
3256
|
+
if not os.path.isfile(harness):
|
|
3257
|
+
print("[qa_ledger] harness not found: %s" % harness, file=sys.stderr)
|
|
3258
|
+
sys.exit(2)
|
|
3259
|
+
|
|
3260
|
+
tmp = tempfile.mkdtemp(prefix="uscha-gc-")
|
|
3261
|
+
try:
|
|
3262
|
+
data_file = os.path.join(tmp, ".coverage")
|
|
3263
|
+
rc = os.path.join(tmp, "cov.rc")
|
|
3264
|
+
with open(rc, "w", encoding="utf-8") as fh:
|
|
3265
|
+
fh.write("[run]\nparallel = True\ndata_file = %s\n"
|
|
3266
|
+
% data_file.replace("\\", "/"))
|
|
3267
|
+
with open(os.path.join(tmp, "sitecustomize.py"), "w", encoding="utf-8") as fh:
|
|
3268
|
+
fh.write("import coverage\ncoverage.process_startup()\n")
|
|
3269
|
+
|
|
3270
|
+
env = dict(os.environ)
|
|
3271
|
+
env["COVERAGE_PROCESS_START"] = rc
|
|
3272
|
+
env["PYTHONPATH"] = tmp + os.pathsep + env.get("PYTHONPATH", "")
|
|
3273
|
+
env["PYTHONIOENCODING"] = "utf-8"
|
|
3274
|
+
r = subprocess.run([sys.executable, harness], cwd=root, env=env,
|
|
3275
|
+
capture_output=True, text=True, encoding="utf-8",
|
|
3276
|
+
errors="replace")
|
|
3277
|
+
if r.returncode != 0:
|
|
3278
|
+
print("[qa_ledger] the harness failed (exit %d) - no map recorded from a run "
|
|
3279
|
+
"that did not complete:\n%s" % (r.returncode, (r.stderr or "")[-1500:]),
|
|
3280
|
+
file=sys.stderr)
|
|
3281
|
+
sys.exit(2)
|
|
3282
|
+
|
|
3283
|
+
cov = coverage.Coverage(data_file=data_file)
|
|
3284
|
+
try:
|
|
3285
|
+
cov.combine()
|
|
3286
|
+
cov.save()
|
|
3287
|
+
except Exception as exc:
|
|
3288
|
+
# Never silent: a PARTIAL combine yields an incomplete-but-non-empty file list,
|
|
3289
|
+
# which slips past the empty-map guard below and records a map that under-reports
|
|
3290
|
+
# what the golden covers. The empty case still exits 2; this one is announced so a
|
|
3291
|
+
# human sees the map may be short (fresh-review finding).
|
|
3292
|
+
print("[qa_ledger] coverage combine reported: %s - the map below may be "
|
|
3293
|
+
"incomplete; re-run before trusting it." % exc, file=sys.stderr)
|
|
3294
|
+
measured = sorted(cov.get_data().measured_files())
|
|
3295
|
+
harness_rel = _gc_rel(harness, root)
|
|
3296
|
+
files = []
|
|
3297
|
+
for m in measured:
|
|
3298
|
+
rel = _gc_rel(os.path.abspath(m), root)
|
|
3299
|
+
# the harness measures the SUBJECT, not itself; sitecustomize is our scaffolding
|
|
3300
|
+
if not rel or rel == harness_rel or rel.endswith("/sitecustomize.py"):
|
|
3301
|
+
continue
|
|
3302
|
+
files.append(rel)
|
|
3303
|
+
files = sorted(set(files))
|
|
3304
|
+
finally:
|
|
3305
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
3306
|
+
|
|
3307
|
+
if not files:
|
|
3308
|
+
print("[qa_ledger] the run measured no source file inside %s - refusing to record "
|
|
3309
|
+
"an empty map (it would read as 'covers nothing')." % root, file=sys.stderr)
|
|
3310
|
+
sys.exit(2)
|
|
3311
|
+
|
|
3312
|
+
head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=root,
|
|
3313
|
+
capture_output=True, text=True)
|
|
3314
|
+
commit = head.stdout.strip() if head.returncode == 0 else None
|
|
3315
|
+
golden_rel = _gc_rel(os.path.abspath(args.golden), root) or args.golden
|
|
3316
|
+
|
|
3317
|
+
path = os.path.join(root, GOLDEN_COVERAGE_FILE)
|
|
3318
|
+
manifest = _load_golden_coverage(root) or {"goldens": {}}
|
|
3319
|
+
manifest["goldens"][golden_rel] = {
|
|
3320
|
+
"harness": harness_rel,
|
|
3321
|
+
"files": files,
|
|
3322
|
+
"captured_at": _now(),
|
|
3323
|
+
"captured_at_commit": commit,
|
|
3324
|
+
"tool": "coverage.py " + coverage.__version__,
|
|
3325
|
+
}
|
|
3326
|
+
with open(path, "w", encoding="utf-8", newline="\n") as fh:
|
|
3327
|
+
json.dump(manifest, fh, indent=2, ensure_ascii=False, sort_keys=True)
|
|
3328
|
+
fh.write("\n")
|
|
3329
|
+
|
|
3330
|
+
if args.json:
|
|
3331
|
+
print(json.dumps(manifest["goldens"][golden_rel], indent=2, ensure_ascii=False))
|
|
3332
|
+
else:
|
|
3333
|
+
print("GOLDEN-COVERAGE %s: %d source file(s) measured -> %s"
|
|
3334
|
+
% (golden_rel, len(files), GOLDEN_COVERAGE_FILE))
|
|
3335
|
+
for f in files[:20]:
|
|
3336
|
+
print(" " + f)
|
|
3337
|
+
|
|
3338
|
+
|
|
3339
|
+
|
|
3075
3340
|
def cmd_escalate(args):
|
|
3076
3341
|
ledger = _load(args.ledger)
|
|
3077
3342
|
_repo_node(ledger, args.repo)
|
|
@@ -4108,6 +4373,16 @@ def cmd_dashboard(args):
|
|
|
4108
4373
|
for r in {e.get("repo") for e in ledger["fast_path"]}}
|
|
4109
4374
|
if ledger.get("spec_drift"):
|
|
4110
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
|
|
4111
4386
|
if getattr(args, "json", False):
|
|
4112
4387
|
print(json.dumps(out, indent=2, ensure_ascii=False))
|
|
4113
4388
|
return
|
|
@@ -6812,6 +7087,14 @@ def build_parser():
|
|
|
6812
7087
|
pfp.add_argument("--json", action="store_true")
|
|
6813
7088
|
pfp.set_defaults(func=cmd_fastpath_eval)
|
|
6814
7089
|
|
|
7090
|
+
pgc = sub.add_parser("golden-coverage",
|
|
7091
|
+
help="record the MEASURED source files a golden's harness exercises (ADR-006)")
|
|
7092
|
+
pgc.add_argument("--harness", required=True, help="script that drives the subject")
|
|
7093
|
+
pgc.add_argument("--golden", required=True, help="the golden this map belongs to")
|
|
7094
|
+
pgc.add_argument("--dir", default=".", help="repo root holding " + GOLDEN_COVERAGE_FILE)
|
|
7095
|
+
pgc.add_argument("--json", action="store_true")
|
|
7096
|
+
pgc.set_defaults(func=cmd_golden_coverage)
|
|
7097
|
+
|
|
6815
7098
|
psd = sub.add_parser("spec-drift",
|
|
6816
7099
|
help="advisory spec-vs-code drift from git commit dates (ADR-005); never gates, exit 0 always")
|
|
6817
7100
|
psd.add_argument("--ledger", default="QA-LEDGER.json")
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
# mirador-watch.ps1 -- live second-screen mirador (uscha-kit 1.34.0), Windows.
|
|
2
|
-
# Regenerates mirador.html every N seconds from the current ledger; the page is rendered
|
|
3
|
-
# with a meta-refresh at the same interval, so a browser open on it updates on its own.
|
|
4
|
-
#
|
|
5
|
-
# Usage (run in a spare terminal, from the project root):
|
|
6
|
-
# powershell -NoProfile -File <kit>\.claude\skills\uscha-mirador\mirador-watch.ps1 [-Interval 30]
|
|
7
|
-
# then open mirador.html in a browser on your second screen. Ctrl-C to stop.
|
|
8
|
-
#
|
|
9
|
-
# Overridable via env: ENGINE, LEDGER, TEMPLATE, OUT, PYTHON.
|
|
10
|
-
param([int]$Interval = 30)
|
|
11
|
-
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
12
|
-
$engine = if ($env:ENGINE) { $env:ENGINE } else { Join-Path $here "..\uscha-devloop\qa_ledger.py" }
|
|
13
|
-
$ledger = if ($env:LEDGER) { $env:LEDGER } else { "QA-LEDGER.json" }
|
|
14
|
-
$template = if ($env:TEMPLATE) { $env:TEMPLATE } else { Join-Path $here "mirador.template.html" }
|
|
15
|
-
$out = if ($env:OUT) { $env:OUT } else { "mirador.html" }
|
|
16
|
-
$py = if ($env:PYTHON) { $env:PYTHON } else { "python" }
|
|
17
|
-
Write-Host "mirador-watch: regenerating $out every ${Interval}s (Ctrl-C to stop). Open $out in a browser."
|
|
18
|
-
while ($true) {
|
|
19
|
-
& $py (Join-Path $here "mirador-render.py") --engine $engine --ledger $ledger --template $template --out $out --refresh $Interval --no-open
|
|
20
|
-
if ($LASTEXITCODE -ne 0) { Write-Host "mirador-watch: render failed (ledger missing? run uscha-devloop first) -- retrying" }
|
|
21
|
-
Start-Sleep -Seconds $Interval
|
|
22
|
-
}
|
|
1
|
+
# mirador-watch.ps1 -- live second-screen mirador (uscha-kit 1.34.0), Windows.
|
|
2
|
+
# Regenerates mirador.html every N seconds from the current ledger; the page is rendered
|
|
3
|
+
# with a meta-refresh at the same interval, so a browser open on it updates on its own.
|
|
4
|
+
#
|
|
5
|
+
# Usage (run in a spare terminal, from the project root):
|
|
6
|
+
# powershell -NoProfile -File <kit>\.claude\skills\uscha-mirador\mirador-watch.ps1 [-Interval 30]
|
|
7
|
+
# then open mirador.html in a browser on your second screen. Ctrl-C to stop.
|
|
8
|
+
#
|
|
9
|
+
# Overridable via env: ENGINE, LEDGER, TEMPLATE, OUT, PYTHON.
|
|
10
|
+
param([int]$Interval = 30)
|
|
11
|
+
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
12
|
+
$engine = if ($env:ENGINE) { $env:ENGINE } else { Join-Path $here "..\uscha-devloop\qa_ledger.py" }
|
|
13
|
+
$ledger = if ($env:LEDGER) { $env:LEDGER } else { "QA-LEDGER.json" }
|
|
14
|
+
$template = if ($env:TEMPLATE) { $env:TEMPLATE } else { Join-Path $here "mirador.template.html" }
|
|
15
|
+
$out = if ($env:OUT) { $env:OUT } else { "mirador.html" }
|
|
16
|
+
$py = if ($env:PYTHON) { $env:PYTHON } else { "python" }
|
|
17
|
+
Write-Host "mirador-watch: regenerating $out every ${Interval}s (Ctrl-C to stop). Open $out in a browser."
|
|
18
|
+
while ($true) {
|
|
19
|
+
& $py (Join-Path $here "mirador-render.py") --engine $engine --ledger $ledger --template $template --out $out --refresh $Interval --no-open
|
|
20
|
+
if ($LASTEXITCODE -ne 0) { Write-Host "mirador-watch: render failed (ledger missing? run uscha-devloop first) -- retrying" }
|
|
21
|
+
Start-Sleep -Seconds $Interval
|
|
22
|
+
}
|
|
File without changes
|
|
@@ -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.
|
|
2
|
+
"version": "1.61.0",
|
|
3
3
|
"project": null,
|
|
4
4
|
"defaults": {
|
|
5
5
|
"coverage_threshold": 60,
|
|
@@ -49,7 +49,8 @@
|
|
|
49
49
|
"**/*.approved",
|
|
50
50
|
"db/**"
|
|
51
51
|
],
|
|
52
|
-
"require_asserting_test": true
|
|
52
|
+
"require_asserting_test": true,
|
|
53
|
+
"forbid_when_golden_touched": false
|
|
53
54
|
},
|
|
54
55
|
"spec_drift": {
|
|
55
56
|
"max_lag_days": 30
|
|
File without changes
|