@andresmassello/uscha 1.59.0 → 1.60.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.59.0** <!-- uscha:version --> · [uscha.dev](https://uscha.dev) ·
43
+ **Kit v1.60.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
 
@@ -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`, 31 subcommands, Python stdlib) that ingests
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.59.0",
3
+ "version": "1.60.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",
@@ -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
@@ -2847,6 +2848,64 @@ def cmd_fastpath_eval(args):
2847
2848
  sig("protected_paths", hits if hits else 0,
2848
2849
  "no touched file matches a protected glob", "config globs over " + src,
2849
2850
  not hits)
2851
+ # ADR-006: the golden-touched veto. OPT-IN -- absent flag, no signal at all
2852
+ # and behavior identical to 1.57.0+. DECLARED -- fail-closed: a missing or
2853
+ # empty mapping DENIES, because "could not measure" never grants a shortcut.
2854
+ if fp.get("forbid_when_golden_touched"):
2855
+ gcm = _load_golden_coverage(repo_path) # malformed -> exit 2, never silent
2856
+ gmap = (gcm or {}).get("goldens") or {}
2857
+ # Enumerate the goldens that actually EXIST. A manifest knowing only SOME
2858
+ # of them would otherwise assert "no touched file is covered by a golden"
2859
+ # about goldens it has never measured -- an ALLOW built on ignorance, which
2860
+ # is precisely the silent bypass this veto exists to prevent. Found by
2861
+ # fresh review and reproduced: 2 goldens in the tree, 1 in the map, a diff
2862
+ # touching the unmapped one's source -> ALLOW. Same glob shape cmd_golden_diff
2863
+ # already uses to locate goldens; no new mechanism.
2864
+ _sfx = ".appro" + "ved"
2865
+ _hits = set(glob.glob(os.path.join(repo_path, "**", "*" + _sfx),
2866
+ recursive=True))
2867
+ _hits |= set(glob.glob(os.path.join(repo_path, "**", "*" + _sfx + ".*"),
2868
+ recursive=True))
2869
+ _tree = set()
2870
+ for _p in _hits:
2871
+ if os.path.isfile(_p):
2872
+ _r = _gc_rel(os.path.abspath(_p), os.path.abspath(repo_path))
2873
+ if _r:
2874
+ _tree.add(_r)
2875
+ _unmapped = sorted(_tree - set(gmap))
2876
+ if _unmapped:
2877
+ # covers the manifest-absent case too: with goldens present and no
2878
+ # manifest, every one of them is unmapped.
2879
+ sig("golden_touched", _unmapped[:5],
2880
+ "every golden in the tree carries a measured map",
2881
+ GOLDEN_COVERAGE_FILE + " (missing or incomplete -- run "
2882
+ "golden-coverage for each golden)", False)
2883
+ elif not _tree:
2884
+ # No golden exists, so none can be touched. This is a MEASUREMENT
2885
+ # ("nothing to cover"), not an absence of one -- denying forever a
2886
+ # repo that has no goldens would be ceremony, not rigor.
2887
+ sig("golden_touched", 0, "no golden in the tree to be covered",
2888
+ "glob over " + repo_path, True)
2889
+ else:
2890
+ covered = {}
2891
+ _commits, _tools = set(), set()
2892
+ for _g, _e in gcm["goldens"].items():
2893
+ for _f in _e.get("files", []):
2894
+ covered.setdefault(_f, []).append(_g)
2895
+ if _e.get("captured_at_commit"):
2896
+ _commits.add(_e["captured_at_commit"][:8])
2897
+ if _e.get("tool"):
2898
+ _tools.add(_e["tool"])
2899
+ ghits = ["%s (golden: %s)" % (f, ", ".join(covered[f]))
2900
+ for f in files if f in covered]
2901
+ # provenance travels with the verdict (ADR-006: no freshness gate,
2902
+ # but every verdict says which capture it trusted)
2903
+ prov = "%s @ %s (%s)" % (
2904
+ GOLDEN_COVERAGE_FILE,
2905
+ ",".join(sorted(_commits)) if _commits else "no commit recorded",
2906
+ ", ".join(sorted(_tools)) if _tools else "no tool recorded")
2907
+ sig("golden_touched", ghits if ghits else 0,
2908
+ "no touched file is covered by a golden", prov, not ghits)
2850
2909
 
2851
2910
  verdict = "ALLOW" if not deny else "DENY"
2852
2911
  # Escalation means "an ACTIVE fast-path run outgrew its thresholds" -- so it gates on the
@@ -3072,6 +3131,158 @@ def cmd_spec_drift(args):
3072
3131
 
3073
3132
 
3074
3133
 
3134
+ # --------------------------------------------------------------------------- #
3135
+ # golden-coverage (ADR-006: the golden<->source mapping, DERIVED BY MEASUREMENT)
3136
+ # --------------------------------------------------------------------------- #
3137
+
3138
+ GOLDEN_COVERAGE_FILE = "golden.coverage.json"
3139
+
3140
+
3141
+ def _load_golden_coverage(root):
3142
+ """Read the measured golden<->source manifest. Strict shape, mirroring
3143
+ _load_scrub_rules: a typo must NOT degrade into "no mapping" in silence, because
3144
+ under a declared veto that silence would GRANT the shortcut it exists to deny.
3145
+ Absent file -> None (the caller decides; with the veto declared, absent is DENY)."""
3146
+ path = os.path.join(root, GOLDEN_COVERAGE_FILE)
3147
+ if not os.path.isfile(path):
3148
+ return None
3149
+ try:
3150
+ with open(path, "r", encoding="utf-8") as fh:
3151
+ spec = json.load(fh)
3152
+ if not isinstance(spec, dict) or not isinstance(spec.get("goldens"), dict):
3153
+ raise TypeError('expected {"goldens": {"<golden path>": {"files": [...]}}}')
3154
+ for g, entry in spec["goldens"].items():
3155
+ if not isinstance(entry, dict) or not isinstance(entry.get("files"), list):
3156
+ raise TypeError("golden %r has no files list" % g)
3157
+ for f in entry["files"]:
3158
+ if not isinstance(f, str):
3159
+ raise TypeError("golden %r maps a non-string file" % g)
3160
+ return spec
3161
+ except (json.JSONDecodeError, TypeError, KeyError) as exc:
3162
+ print("[qa_ledger] %s invalid (%s) - the golden mapping is not skipped in "
3163
+ "silence: fix the file or delete it." % (path, exc), file=sys.stderr)
3164
+ sys.exit(2)
3165
+
3166
+
3167
+ def _gc_rel(path, root):
3168
+ # realpath BOTH sides before comparing. On Windows a temp dir under a username longer
3169
+ # than 8 chars is reported in 8.3 short form (RUNNER~1) by one side and long form by the
3170
+ # other; relpath then yields "../.." and a file INSIDE the repo is filtered out as
3171
+ # outside it -- silently shrinking the map. Invisible on a machine whose username does
3172
+ # not mangle (which is why local Windows was green and Windows CI was not).
3173
+ try:
3174
+ rel = os.path.relpath(os.path.realpath(path), os.path.realpath(root))
3175
+ except ValueError: # different drive on Windows -- outside the repo either way
3176
+ return None
3177
+ rel = rel.replace("\\", "/")
3178
+ return None if rel.startswith("../") else rel
3179
+
3180
+
3181
+ def cmd_golden_coverage(args):
3182
+ """Record the MEASURED source files a golden's harness exercises (ADR-006).
3183
+
3184
+ The harnesses drive their subject through subprocess, so instrumenting only the parent
3185
+ measures nothing: coverage is injected into EVERY python the harness spawns via a
3186
+ sitecustomize on PYTHONPATH plus COVERAGE_PROCESS_START -- the documented multiprocess
3187
+ technique, and the same PYTHONPATH-injection shape this repo's fault tests already use.
3188
+
3189
+ coverage.py is an optional CAPTURE-time dependency (the engine stays stdlib-only at
3190
+ runtime). Absent, this writes NOTHING and exits 2: an empty map would read as
3191
+ "this golden covers nothing", which is the one lie that would let the veto pass."""
3192
+ try:
3193
+ import coverage
3194
+ except ImportError:
3195
+ print("[qa_ledger] coverage.py is not installed - refusing to write a map that was "
3196
+ "not measured (an empty map reads as 'covers nothing'). pip install coverage",
3197
+ file=sys.stderr)
3198
+ sys.exit(2)
3199
+
3200
+ root = os.path.abspath(args.dir or ".")
3201
+ harness = os.path.abspath(args.harness)
3202
+ if not os.path.isfile(harness):
3203
+ print("[qa_ledger] harness not found: %s" % harness, file=sys.stderr)
3204
+ sys.exit(2)
3205
+
3206
+ tmp = tempfile.mkdtemp(prefix="uscha-gc-")
3207
+ try:
3208
+ data_file = os.path.join(tmp, ".coverage")
3209
+ rc = os.path.join(tmp, "cov.rc")
3210
+ with open(rc, "w", encoding="utf-8") as fh:
3211
+ fh.write("[run]\nparallel = True\ndata_file = %s\n"
3212
+ % data_file.replace("\\", "/"))
3213
+ with open(os.path.join(tmp, "sitecustomize.py"), "w", encoding="utf-8") as fh:
3214
+ fh.write("import coverage\ncoverage.process_startup()\n")
3215
+
3216
+ env = dict(os.environ)
3217
+ env["COVERAGE_PROCESS_START"] = rc
3218
+ env["PYTHONPATH"] = tmp + os.pathsep + env.get("PYTHONPATH", "")
3219
+ env["PYTHONIOENCODING"] = "utf-8"
3220
+ r = subprocess.run([sys.executable, harness], cwd=root, env=env,
3221
+ capture_output=True, text=True, encoding="utf-8",
3222
+ errors="replace")
3223
+ if r.returncode != 0:
3224
+ print("[qa_ledger] the harness failed (exit %d) - no map recorded from a run "
3225
+ "that did not complete:\n%s" % (r.returncode, (r.stderr or "")[-1500:]),
3226
+ file=sys.stderr)
3227
+ sys.exit(2)
3228
+
3229
+ cov = coverage.Coverage(data_file=data_file)
3230
+ try:
3231
+ cov.combine()
3232
+ cov.save()
3233
+ except Exception as exc:
3234
+ # Never silent: a PARTIAL combine yields an incomplete-but-non-empty file list,
3235
+ # which slips past the empty-map guard below and records a map that under-reports
3236
+ # what the golden covers. The empty case still exits 2; this one is announced so a
3237
+ # human sees the map may be short (fresh-review finding).
3238
+ print("[qa_ledger] coverage combine reported: %s - the map below may be "
3239
+ "incomplete; re-run before trusting it." % exc, file=sys.stderr)
3240
+ measured = sorted(cov.get_data().measured_files())
3241
+ harness_rel = _gc_rel(harness, root)
3242
+ files = []
3243
+ for m in measured:
3244
+ rel = _gc_rel(os.path.abspath(m), root)
3245
+ # the harness measures the SUBJECT, not itself; sitecustomize is our scaffolding
3246
+ if not rel or rel == harness_rel or rel.endswith("/sitecustomize.py"):
3247
+ continue
3248
+ files.append(rel)
3249
+ files = sorted(set(files))
3250
+ finally:
3251
+ shutil.rmtree(tmp, ignore_errors=True)
3252
+
3253
+ if not files:
3254
+ print("[qa_ledger] the run measured no source file inside %s - refusing to record "
3255
+ "an empty map (it would read as 'covers nothing')." % root, file=sys.stderr)
3256
+ sys.exit(2)
3257
+
3258
+ head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=root,
3259
+ capture_output=True, text=True)
3260
+ commit = head.stdout.strip() if head.returncode == 0 else None
3261
+ golden_rel = _gc_rel(os.path.abspath(args.golden), root) or args.golden
3262
+
3263
+ path = os.path.join(root, GOLDEN_COVERAGE_FILE)
3264
+ manifest = _load_golden_coverage(root) or {"goldens": {}}
3265
+ manifest["goldens"][golden_rel] = {
3266
+ "harness": harness_rel,
3267
+ "files": files,
3268
+ "captured_at": _now(),
3269
+ "captured_at_commit": commit,
3270
+ "tool": "coverage.py " + coverage.__version__,
3271
+ }
3272
+ with open(path, "w", encoding="utf-8", newline="\n") as fh:
3273
+ json.dump(manifest, fh, indent=2, ensure_ascii=False, sort_keys=True)
3274
+ fh.write("\n")
3275
+
3276
+ if args.json:
3277
+ print(json.dumps(manifest["goldens"][golden_rel], indent=2, ensure_ascii=False))
3278
+ else:
3279
+ print("GOLDEN-COVERAGE %s: %d source file(s) measured -> %s"
3280
+ % (golden_rel, len(files), GOLDEN_COVERAGE_FILE))
3281
+ for f in files[:20]:
3282
+ print(" " + f)
3283
+
3284
+
3285
+
3075
3286
  def cmd_escalate(args):
3076
3287
  ledger = _load(args.ledger)
3077
3288
  _repo_node(ledger, args.repo)
@@ -6812,6 +7023,14 @@ def build_parser():
6812
7023
  pfp.add_argument("--json", action="store_true")
6813
7024
  pfp.set_defaults(func=cmd_fastpath_eval)
6814
7025
 
7026
+ pgc = sub.add_parser("golden-coverage",
7027
+ help="record the MEASURED source files a golden's harness exercises (ADR-006)")
7028
+ pgc.add_argument("--harness", required=True, help="script that drives the subject")
7029
+ pgc.add_argument("--golden", required=True, help="the golden this map belongs to")
7030
+ pgc.add_argument("--dir", default=".", help="repo root holding " + GOLDEN_COVERAGE_FILE)
7031
+ pgc.add_argument("--json", action="store_true")
7032
+ pgc.set_defaults(func=cmd_golden_coverage)
7033
+
6815
7034
  psd = sub.add_parser("spec-drift",
6816
7035
  help="advisory spec-vs-code drift from git commit dates (ADR-005); never gates, exit 0 always")
6817
7036
  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
+ }
@@ -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.59.0",
4
+ "version": "1.60.1",
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, 31 subcommands + universal installer + npm/npx router). Facts block, guesses advise; the human approves.",
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"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uscha",
3
- "version": "1.59.0",
3
+ "version": "1.60.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,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.
@@ -1,6 +1,6 @@
1
1
  # uscha-kit
2
2
 
3
- **Kit version:** v1.59.0 <!-- uscha:version --> · **[uscha.dev](https://uscha.dev)**
3
+ **Kit version:** v1.60.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`,
@@ -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
package/uscha-kit/VERSION CHANGED
@@ -1 +1 @@
1
- uscha-kit 1.59.0
1
+ uscha-kit 1.60.1
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}
@@ -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
@@ -2847,6 +2848,64 @@ def cmd_fastpath_eval(args):
2847
2848
  sig("protected_paths", hits if hits else 0,
2848
2849
  "no touched file matches a protected glob", "config globs over " + src,
2849
2850
  not hits)
2851
+ # ADR-006: the golden-touched veto. OPT-IN -- absent flag, no signal at all
2852
+ # and behavior identical to 1.57.0+. DECLARED -- fail-closed: a missing or
2853
+ # empty mapping DENIES, because "could not measure" never grants a shortcut.
2854
+ if fp.get("forbid_when_golden_touched"):
2855
+ gcm = _load_golden_coverage(repo_path) # malformed -> exit 2, never silent
2856
+ gmap = (gcm or {}).get("goldens") or {}
2857
+ # Enumerate the goldens that actually EXIST. A manifest knowing only SOME
2858
+ # of them would otherwise assert "no touched file is covered by a golden"
2859
+ # about goldens it has never measured -- an ALLOW built on ignorance, which
2860
+ # is precisely the silent bypass this veto exists to prevent. Found by
2861
+ # fresh review and reproduced: 2 goldens in the tree, 1 in the map, a diff
2862
+ # touching the unmapped one's source -> ALLOW. Same glob shape cmd_golden_diff
2863
+ # already uses to locate goldens; no new mechanism.
2864
+ _sfx = ".appro" + "ved"
2865
+ _hits = set(glob.glob(os.path.join(repo_path, "**", "*" + _sfx),
2866
+ recursive=True))
2867
+ _hits |= set(glob.glob(os.path.join(repo_path, "**", "*" + _sfx + ".*"),
2868
+ recursive=True))
2869
+ _tree = set()
2870
+ for _p in _hits:
2871
+ if os.path.isfile(_p):
2872
+ _r = _gc_rel(os.path.abspath(_p), os.path.abspath(repo_path))
2873
+ if _r:
2874
+ _tree.add(_r)
2875
+ _unmapped = sorted(_tree - set(gmap))
2876
+ if _unmapped:
2877
+ # covers the manifest-absent case too: with goldens present and no
2878
+ # manifest, every one of them is unmapped.
2879
+ sig("golden_touched", _unmapped[:5],
2880
+ "every golden in the tree carries a measured map",
2881
+ GOLDEN_COVERAGE_FILE + " (missing or incomplete -- run "
2882
+ "golden-coverage for each golden)", False)
2883
+ elif not _tree:
2884
+ # No golden exists, so none can be touched. This is a MEASUREMENT
2885
+ # ("nothing to cover"), not an absence of one -- denying forever a
2886
+ # repo that has no goldens would be ceremony, not rigor.
2887
+ sig("golden_touched", 0, "no golden in the tree to be covered",
2888
+ "glob over " + repo_path, True)
2889
+ else:
2890
+ covered = {}
2891
+ _commits, _tools = set(), set()
2892
+ for _g, _e in gcm["goldens"].items():
2893
+ for _f in _e.get("files", []):
2894
+ covered.setdefault(_f, []).append(_g)
2895
+ if _e.get("captured_at_commit"):
2896
+ _commits.add(_e["captured_at_commit"][:8])
2897
+ if _e.get("tool"):
2898
+ _tools.add(_e["tool"])
2899
+ ghits = ["%s (golden: %s)" % (f, ", ".join(covered[f]))
2900
+ for f in files if f in covered]
2901
+ # provenance travels with the verdict (ADR-006: no freshness gate,
2902
+ # but every verdict says which capture it trusted)
2903
+ prov = "%s @ %s (%s)" % (
2904
+ GOLDEN_COVERAGE_FILE,
2905
+ ",".join(sorted(_commits)) if _commits else "no commit recorded",
2906
+ ", ".join(sorted(_tools)) if _tools else "no tool recorded")
2907
+ sig("golden_touched", ghits if ghits else 0,
2908
+ "no touched file is covered by a golden", prov, not ghits)
2850
2909
 
2851
2910
  verdict = "ALLOW" if not deny else "DENY"
2852
2911
  # Escalation means "an ACTIVE fast-path run outgrew its thresholds" -- so it gates on the
@@ -3072,6 +3131,158 @@ def cmd_spec_drift(args):
3072
3131
 
3073
3132
 
3074
3133
 
3134
+ # --------------------------------------------------------------------------- #
3135
+ # golden-coverage (ADR-006: the golden<->source mapping, DERIVED BY MEASUREMENT)
3136
+ # --------------------------------------------------------------------------- #
3137
+
3138
+ GOLDEN_COVERAGE_FILE = "golden.coverage.json"
3139
+
3140
+
3141
+ def _load_golden_coverage(root):
3142
+ """Read the measured golden<->source manifest. Strict shape, mirroring
3143
+ _load_scrub_rules: a typo must NOT degrade into "no mapping" in silence, because
3144
+ under a declared veto that silence would GRANT the shortcut it exists to deny.
3145
+ Absent file -> None (the caller decides; with the veto declared, absent is DENY)."""
3146
+ path = os.path.join(root, GOLDEN_COVERAGE_FILE)
3147
+ if not os.path.isfile(path):
3148
+ return None
3149
+ try:
3150
+ with open(path, "r", encoding="utf-8") as fh:
3151
+ spec = json.load(fh)
3152
+ if not isinstance(spec, dict) or not isinstance(spec.get("goldens"), dict):
3153
+ raise TypeError('expected {"goldens": {"<golden path>": {"files": [...]}}}')
3154
+ for g, entry in spec["goldens"].items():
3155
+ if not isinstance(entry, dict) or not isinstance(entry.get("files"), list):
3156
+ raise TypeError("golden %r has no files list" % g)
3157
+ for f in entry["files"]:
3158
+ if not isinstance(f, str):
3159
+ raise TypeError("golden %r maps a non-string file" % g)
3160
+ return spec
3161
+ except (json.JSONDecodeError, TypeError, KeyError) as exc:
3162
+ print("[qa_ledger] %s invalid (%s) - the golden mapping is not skipped in "
3163
+ "silence: fix the file or delete it." % (path, exc), file=sys.stderr)
3164
+ sys.exit(2)
3165
+
3166
+
3167
+ def _gc_rel(path, root):
3168
+ # realpath BOTH sides before comparing. On Windows a temp dir under a username longer
3169
+ # than 8 chars is reported in 8.3 short form (RUNNER~1) by one side and long form by the
3170
+ # other; relpath then yields "../.." and a file INSIDE the repo is filtered out as
3171
+ # outside it -- silently shrinking the map. Invisible on a machine whose username does
3172
+ # not mangle (which is why local Windows was green and Windows CI was not).
3173
+ try:
3174
+ rel = os.path.relpath(os.path.realpath(path), os.path.realpath(root))
3175
+ except ValueError: # different drive on Windows -- outside the repo either way
3176
+ return None
3177
+ rel = rel.replace("\\", "/")
3178
+ return None if rel.startswith("../") else rel
3179
+
3180
+
3181
+ def cmd_golden_coverage(args):
3182
+ """Record the MEASURED source files a golden's harness exercises (ADR-006).
3183
+
3184
+ The harnesses drive their subject through subprocess, so instrumenting only the parent
3185
+ measures nothing: coverage is injected into EVERY python the harness spawns via a
3186
+ sitecustomize on PYTHONPATH plus COVERAGE_PROCESS_START -- the documented multiprocess
3187
+ technique, and the same PYTHONPATH-injection shape this repo's fault tests already use.
3188
+
3189
+ coverage.py is an optional CAPTURE-time dependency (the engine stays stdlib-only at
3190
+ runtime). Absent, this writes NOTHING and exits 2: an empty map would read as
3191
+ "this golden covers nothing", which is the one lie that would let the veto pass."""
3192
+ try:
3193
+ import coverage
3194
+ except ImportError:
3195
+ print("[qa_ledger] coverage.py is not installed - refusing to write a map that was "
3196
+ "not measured (an empty map reads as 'covers nothing'). pip install coverage",
3197
+ file=sys.stderr)
3198
+ sys.exit(2)
3199
+
3200
+ root = os.path.abspath(args.dir or ".")
3201
+ harness = os.path.abspath(args.harness)
3202
+ if not os.path.isfile(harness):
3203
+ print("[qa_ledger] harness not found: %s" % harness, file=sys.stderr)
3204
+ sys.exit(2)
3205
+
3206
+ tmp = tempfile.mkdtemp(prefix="uscha-gc-")
3207
+ try:
3208
+ data_file = os.path.join(tmp, ".coverage")
3209
+ rc = os.path.join(tmp, "cov.rc")
3210
+ with open(rc, "w", encoding="utf-8") as fh:
3211
+ fh.write("[run]\nparallel = True\ndata_file = %s\n"
3212
+ % data_file.replace("\\", "/"))
3213
+ with open(os.path.join(tmp, "sitecustomize.py"), "w", encoding="utf-8") as fh:
3214
+ fh.write("import coverage\ncoverage.process_startup()\n")
3215
+
3216
+ env = dict(os.environ)
3217
+ env["COVERAGE_PROCESS_START"] = rc
3218
+ env["PYTHONPATH"] = tmp + os.pathsep + env.get("PYTHONPATH", "")
3219
+ env["PYTHONIOENCODING"] = "utf-8"
3220
+ r = subprocess.run([sys.executable, harness], cwd=root, env=env,
3221
+ capture_output=True, text=True, encoding="utf-8",
3222
+ errors="replace")
3223
+ if r.returncode != 0:
3224
+ print("[qa_ledger] the harness failed (exit %d) - no map recorded from a run "
3225
+ "that did not complete:\n%s" % (r.returncode, (r.stderr or "")[-1500:]),
3226
+ file=sys.stderr)
3227
+ sys.exit(2)
3228
+
3229
+ cov = coverage.Coverage(data_file=data_file)
3230
+ try:
3231
+ cov.combine()
3232
+ cov.save()
3233
+ except Exception as exc:
3234
+ # Never silent: a PARTIAL combine yields an incomplete-but-non-empty file list,
3235
+ # which slips past the empty-map guard below and records a map that under-reports
3236
+ # what the golden covers. The empty case still exits 2; this one is announced so a
3237
+ # human sees the map may be short (fresh-review finding).
3238
+ print("[qa_ledger] coverage combine reported: %s - the map below may be "
3239
+ "incomplete; re-run before trusting it." % exc, file=sys.stderr)
3240
+ measured = sorted(cov.get_data().measured_files())
3241
+ harness_rel = _gc_rel(harness, root)
3242
+ files = []
3243
+ for m in measured:
3244
+ rel = _gc_rel(os.path.abspath(m), root)
3245
+ # the harness measures the SUBJECT, not itself; sitecustomize is our scaffolding
3246
+ if not rel or rel == harness_rel or rel.endswith("/sitecustomize.py"):
3247
+ continue
3248
+ files.append(rel)
3249
+ files = sorted(set(files))
3250
+ finally:
3251
+ shutil.rmtree(tmp, ignore_errors=True)
3252
+
3253
+ if not files:
3254
+ print("[qa_ledger] the run measured no source file inside %s - refusing to record "
3255
+ "an empty map (it would read as 'covers nothing')." % root, file=sys.stderr)
3256
+ sys.exit(2)
3257
+
3258
+ head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=root,
3259
+ capture_output=True, text=True)
3260
+ commit = head.stdout.strip() if head.returncode == 0 else None
3261
+ golden_rel = _gc_rel(os.path.abspath(args.golden), root) or args.golden
3262
+
3263
+ path = os.path.join(root, GOLDEN_COVERAGE_FILE)
3264
+ manifest = _load_golden_coverage(root) or {"goldens": {}}
3265
+ manifest["goldens"][golden_rel] = {
3266
+ "harness": harness_rel,
3267
+ "files": files,
3268
+ "captured_at": _now(),
3269
+ "captured_at_commit": commit,
3270
+ "tool": "coverage.py " + coverage.__version__,
3271
+ }
3272
+ with open(path, "w", encoding="utf-8", newline="\n") as fh:
3273
+ json.dump(manifest, fh, indent=2, ensure_ascii=False, sort_keys=True)
3274
+ fh.write("\n")
3275
+
3276
+ if args.json:
3277
+ print(json.dumps(manifest["goldens"][golden_rel], indent=2, ensure_ascii=False))
3278
+ else:
3279
+ print("GOLDEN-COVERAGE %s: %d source file(s) measured -> %s"
3280
+ % (golden_rel, len(files), GOLDEN_COVERAGE_FILE))
3281
+ for f in files[:20]:
3282
+ print(" " + f)
3283
+
3284
+
3285
+
3075
3286
  def cmd_escalate(args):
3076
3287
  ledger = _load(args.ledger)
3077
3288
  _repo_node(ledger, args.repo)
@@ -6812,6 +7023,14 @@ def build_parser():
6812
7023
  pfp.add_argument("--json", action="store_true")
6813
7024
  pfp.set_defaults(func=cmd_fastpath_eval)
6814
7025
 
7026
+ pgc = sub.add_parser("golden-coverage",
7027
+ help="record the MEASURED source files a golden's harness exercises (ADR-006)")
7028
+ pgc.add_argument("--harness", required=True, help="script that drives the subject")
7029
+ pgc.add_argument("--golden", required=True, help="the golden this map belongs to")
7030
+ pgc.add_argument("--dir", default=".", help="repo root holding " + GOLDEN_COVERAGE_FILE)
7031
+ pgc.add_argument("--json", action="store_true")
7032
+ pgc.set_defaults(func=cmd_golden_coverage)
7033
+
6815
7034
  psd = sub.add_parser("spec-drift",
6816
7035
  help="advisory spec-vs-code drift from git commit dates (ADR-005); never gates, exit 0 always")
6817
7036
  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
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.59.0",
2
+ "version": "1.60.1",
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