@andresmassello/uscha 1.56.1 → 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.
Files changed (30) hide show
  1. package/README.md +3 -3
  2. package/bin/uscha.js +0 -0
  3. package/package.json +2 -1
  4. package/uscha-kit/.claude/skills/uscha-characterize/SKILL.md +18 -0
  5. package/uscha-kit/.claude/skills/uscha-devloop/SKILL.md +21 -0
  6. package/uscha-kit/.claude/skills/uscha-devloop/qa_ledger.py +636 -0
  7. package/uscha-kit/.claude/skills/uscha-mirador/SKILL.md +11 -0
  8. package/uscha-kit/.claude/skills/uscha-mirador/mirador-watch.ps1 +22 -22
  9. package/uscha-kit/.claude/skills/uscha-mirador/mirador-watch.sh +0 -0
  10. package/uscha-kit/.claude/skills/uscha-mirador/mirador.template.html +39 -1
  11. package/uscha-kit/.claude/skills/uscha-status/SKILL.md +8 -0
  12. package/uscha-kit/.claude-plugin/plugin.json +2 -2
  13. package/uscha-kit/.codex-plugin/plugin.json +1 -1
  14. package/uscha-kit/INSTALL.md +128 -128
  15. package/uscha-kit/README.md +90 -1
  16. package/uscha-kit/VERSION +1 -1
  17. package/uscha-kit/hooks/block-approved-writes.py +0 -0
  18. package/uscha-kit/reports/junit/.fastpath-cases.json +1 -0
  19. package/uscha-kit/reports/junit/.goldencov-cases.json +1 -0
  20. package/uscha-kit/reports/junit/.specdrift-cases.json +1 -0
  21. package/uscha-kit/skills/uscha-characterize/SKILL.md +18 -0
  22. package/uscha-kit/skills/uscha-devloop/SKILL.md +21 -0
  23. package/uscha-kit/skills/uscha-devloop/qa_ledger.py +636 -0
  24. package/uscha-kit/skills/uscha-mirador/SKILL.md +11 -0
  25. package/uscha-kit/skills/uscha-mirador/mirador-watch.ps1 +22 -22
  26. package/uscha-kit/skills/uscha-mirador/mirador-watch.sh +0 -0
  27. package/uscha-kit/skills/uscha-mirador/mirador.template.html +39 -1
  28. package/uscha-kit/skills/uscha-status/SKILL.md +8 -0
  29. package/uscha-kit/uscha.config.json +16 -1
  30. package/uscha-kit/workbench-doctor.sh +0 -0
@@ -59,7 +59,9 @@ import math
59
59
  import os
60
60
  import re
61
61
  import shutil
62
+ import subprocess
62
63
  import sys
64
+ import tempfile
63
65
  import unicodedata
64
66
  import xml.etree.ElementTree as ET
65
67
  from datetime import datetime, timezone
@@ -2699,6 +2701,588 @@ def cmd_oscillation(args):
2699
2701
  sys.exit(1 if osc else 0)
2700
2702
 
2701
2703
 
2704
+ # --------------------------------------------------------------------------- #
2705
+ # fastpath-eval (ADR-003: fast-path entry by MEASURED signals, never opinion)
2706
+ # --------------------------------------------------------------------------- #
2707
+
2708
+ def _fp_glob_re(g):
2709
+ """Translate a protected-path glob to a regex. `**` crosses directories, `*`/`?` do not.
2710
+ Case-insensitive on purpose: Windows and macOS filesystems are."""
2711
+ g = g.replace("\\", "/")
2712
+ out, i = [], 0
2713
+ while i < len(g):
2714
+ c = g[i]
2715
+ if c == "*":
2716
+ if g[i:i + 2] == "**":
2717
+ i += 2
2718
+ if i < len(g) and g[i] == "/":
2719
+ # `**/` = any number of WHOLE directories (incl. zero) -- segment-anchored,
2720
+ # so `**/migrations/**` does not match `db_migrations/` by substring.
2721
+ out.append("(?:[^/]*/)*")
2722
+ i += 1
2723
+ else:
2724
+ out.append(".*")
2725
+ continue
2726
+ out.append("[^/]*")
2727
+ elif c == "?":
2728
+ out.append("[^/]")
2729
+ elif c in ".^$+{}[]|()":
2730
+ out.append("\\" + c)
2731
+ else:
2732
+ out.append(c)
2733
+ i += 1
2734
+ return re.compile("^(?:%s)$" % "".join(out), re.I)
2735
+
2736
+
2737
+ def cmd_fastpath_eval(args):
2738
+ """Measured verdict for the fast-path (ADR-003). ALLOW only when every signal passes;
2739
+ ANY ambiguity -- no config, no git, unresolvable base -- is DENY with the reason named
2740
+ (fail-closed: "could not measure" never grants the shortcut). With --intent the verdict is
2741
+ recorded in the ledger as a first-class entry; without it this is a dry-run. A prior ALLOW
2742
+ followed by a DENY re-eval escalates through the EXISTING escalation machinery, so the
2743
+ derived phase flips to `escalated` and pr-ready is blocked until the human resolves."""
2744
+ ledger = _load(args.ledger)
2745
+ _repo_node(ledger, args.repo)
2746
+ fp = ledger["config"].get("defaults", {}).get("fast_path")
2747
+ intent = (args.intent or "").strip()
2748
+ signals, deny = [], []
2749
+
2750
+ def sig(name, value, threshold, source, ok):
2751
+ signals.append({"name": name, "value": value, "threshold": threshold,
2752
+ "source": source, "at": _now(), "ok": bool(ok)})
2753
+ if not ok:
2754
+ deny.append(name)
2755
+
2756
+ if not isinstance(fp, dict) or fp.get("enabled") is False:
2757
+ sig("configured", False, "defaults.fast_path present and enabled",
2758
+ "config.defaults.fast_path", False)
2759
+ else:
2760
+ repo_path = _repo_cfg(ledger, args.repo).get("path", ".")
2761
+ base, base_src = args.base, "--base"
2762
+ if base:
2763
+ probe = subprocess.run(["git", "rev-parse", "--verify", base + "^{commit}"],
2764
+ cwd=repo_path, capture_output=True, text=True)
2765
+ if probe.returncode != 0:
2766
+ base = None
2767
+ base_src = "--base (unresolvable)"
2768
+ else:
2769
+ for cand in ("origin/main", "main"):
2770
+ r = subprocess.run(["git", "merge-base", "HEAD", cand], cwd=repo_path,
2771
+ capture_output=True, text=True)
2772
+ if r.returncode == 0 and r.stdout.strip():
2773
+ base, base_src = r.stdout.strip(), "merge-base HEAD %s" % cand
2774
+ break
2775
+ if not base:
2776
+ sig("base_ref", None, "a resolvable base commit",
2777
+ base_src if base_src != "--base" else "git merge-base HEAD origin/main|main",
2778
+ False)
2779
+ else:
2780
+ num = subprocess.run(["git", "diff", "--numstat", base], cwd=repo_path,
2781
+ capture_output=True, text=True, encoding="utf-8",
2782
+ errors="replace")
2783
+ if num.returncode != 0:
2784
+ sig("git_diff", None, "a readable diff",
2785
+ "git diff --numstat %s" % base[:12], False)
2786
+ else:
2787
+ files, loc, binaries = [], 0, []
2788
+ for line in num.stdout.splitlines():
2789
+ parts = line.split("\t")
2790
+ if len(parts) != 3:
2791
+ continue
2792
+ a, d, path = parts
2793
+ path = path.strip().replace("\\", "/")
2794
+ # RENAMES arrive as one descriptor -- `old => new` or `pre{old => new}post`.
2795
+ # Matching the raw descriptor against the globs let a rename INTO db/ walk
2796
+ # straight past protected_paths (found by fresh review). Expand it and
2797
+ # count BOTH sides: renaming a file out of a protected area is as
2798
+ # gate-worthy as renaming one in.
2799
+ if " => " in path:
2800
+ m_ = re.match(r"^(.*)\{(.*) => (.*)\}(.*)$", path)
2801
+ if m_:
2802
+ pre, old_, new_, post = m_.groups()
2803
+ files.append((pre + old_ + post).replace("//", "/"))
2804
+ files.append((pre + new_ + post).replace("//", "/"))
2805
+ else:
2806
+ old_, new_ = path.split(" => ", 1)
2807
+ files.append(old_)
2808
+ files.append(new_)
2809
+ else:
2810
+ files.append(path)
2811
+ if a == "-" or d == "-":
2812
+ # binary diff: LOC is UNMEASURABLE, and an unmeasurable signal must
2813
+ # deny, not silently count as zero (fail-closed).
2814
+ binaries.append(path)
2815
+ loc += (int(a) if a.isdigit() else 0) + (int(d) if d.isdigit() else 0)
2816
+ # UNTRACKED files are invisible to `git diff` -- and a small change is very
2817
+ # often a NEW file. Not counting them would under-measure exactly the case
2818
+ # this gate exists for, so they count as files + added lines (fail-closed).
2819
+ unt = subprocess.run(["git", "ls-files", "--others", "--exclude-standard"],
2820
+ cwd=repo_path, capture_output=True, text=True,
2821
+ encoding="utf-8", errors="replace")
2822
+ for path in unt.stdout.splitlines():
2823
+ path = path.strip()
2824
+ if not path:
2825
+ continue
2826
+ files.append(path.replace("\\", "/"))
2827
+ try:
2828
+ with open(os.path.join(repo_path, path), "rb") as fh:
2829
+ loc += fh.read().count(b"\n")
2830
+ except OSError:
2831
+ loc += 1 # an unreadable new file still counts as a change
2832
+ src = "git diff --numstat %s (+ untracked)" % base[:12]
2833
+ max_f = int(fp.get("max_files_changed", 3))
2834
+ max_l = int(fp.get("max_loc_delta", 80))
2835
+ sig("max_files_changed", len(files), max_f, src, len(files) <= max_f)
2836
+ sig("max_loc_delta", loc, max_l, src, loc <= max_l)
2837
+ if binaries:
2838
+ sig("binary_files", binaries[:5], "none (LOC unmeasurable on binary)",
2839
+ src, False)
2840
+ pats = fp.get("protected_paths",
2841
+ ["**/migrations/**", "**/*.appro" + "ved", "db/**"])
2842
+ hits = []
2843
+ for f in files:
2844
+ for g in pats:
2845
+ if _fp_glob_re(g).match(f):
2846
+ hits.append("%s (%s)" % (f, g))
2847
+ break
2848
+ sig("protected_paths", hits if hits else 0,
2849
+ "no touched file matches a protected glob", "config globs over " + src,
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)
2909
+
2910
+ verdict = "ALLOW" if not deny else "DENY"
2911
+ # Escalation means "an ACTIVE fast-path run outgrew its thresholds" -- so it gates on the
2912
+ # LATEST entry for this repo being ALLOW, not on an ALLOW ever having existed. The first
2913
+ # version scanned all history, which misclassified every later unrelated DENY as ESCALATED
2914
+ # forever (found by fresh review, reproduced in ordinary sequential usage).
2915
+ _fp_prior = [e for e in ledger.get("fast_path", []) if e.get("repo") == args.repo]
2916
+ active_allow = bool(_fp_prior) and _fp_prior[-1].get("verdict") == "ALLOW"
2917
+ escalated = bool(intent and active_allow and verdict == "DENY"
2918
+ and "configured" not in deny)
2919
+ if escalated:
2920
+ verdict = "ESCALATED"
2921
+ out = {"repo": args.repo, "mode": "fast_path", "verdict": verdict,
2922
+ "intent": intent or None, "dry_run": not bool(intent), "signals": signals}
2923
+
2924
+ if intent:
2925
+ ledger.setdefault("fast_path", [])
2926
+ ledger["step_counter"] += 1
2927
+ entry = dict(out)
2928
+ entry.pop("dry_run", None)
2929
+ entry.update({"n": ledger["step_counter"], "at": _now()})
2930
+ ledger["fast_path"].append(entry)
2931
+ ledger["steps"].append({"n": ledger["step_counter"], "at": _now(),
2932
+ "kind": "fastpath-eval", "repo": args.repo})
2933
+ if escalated:
2934
+ # reuse the EXISTING escalation machinery: derived phase flips to `escalated`,
2935
+ # pr-ready is blocked and readiness capped until the human resolve-escalation --
2936
+ # after producing ADR + ACCEPTANCE, per ADR-003 (not a full discovery).
2937
+ ledger["step_counter"] += 1
2938
+ ledger["escalations"].append({
2939
+ "n": ledger["step_counter"], "at": _now(), "repo": args.repo,
2940
+ "reason": "fast-path thresholds exceeded mid-run: " + ", ".join(deny)
2941
+ + " -- produce ADR + ACCEPTANCE, then resolve-escalation"})
2942
+ ledger["steps"].append({"n": ledger["step_counter"], "at": _now(),
2943
+ "kind": "escalation", "repo": args.repo})
2944
+ _save(args.ledger, ledger)
2945
+
2946
+ if args.json:
2947
+ print(json.dumps(out, indent=2, ensure_ascii=False))
2948
+ else:
2949
+ print("FASTPATH %s: %s%s" % (args.repo, verdict,
2950
+ "" if intent else " (dry-run: no --intent, nothing recorded)"))
2951
+ for s_ in signals:
2952
+ print(" %s %s: %s / %s [%s]" % ("ok" if s_["ok"] else "!!",
2953
+ s_["name"], s_["value"], s_["threshold"], s_["source"]))
2954
+ if escalated:
2955
+ print(" -> ESCALATED: pr-ready is blocked; produce ADR + ACCEPTANCE, "
2956
+ "then resolve-escalation (a recorded human act)")
2957
+ sys.exit(0 if verdict == "ALLOW" else 1)
2958
+
2959
+
2960
+ # --------------------------------------------------------------------------- #
2961
+ # spec-drift (ADR-005: mechanical drift detection, advisory -- NEVER a gate)
2962
+ # --------------------------------------------------------------------------- #
2963
+
2964
+ def _sd_governs(path):
2965
+ """Parse the `governs:` glob list from a `---` frontmatter block at the top of a
2966
+ markdown file. Returns None when there is no frontmatter or no `governs:` key
2967
+ (UNMAPPED -- absence of a mapping is absence of measurement, not "no drift")."""
2968
+ try:
2969
+ with open(path, encoding="utf-8", errors="replace") as fh:
2970
+ lines = fh.read().splitlines()
2971
+ except OSError:
2972
+ return None
2973
+ if not lines or lines[0].strip() != "---":
2974
+ return None
2975
+ globs, in_governs = None, False
2976
+ # scan runs to the CLOSING fence, not an arbitrary window -- a governs: key late in a
2977
+ # long frontmatter block must not silently read as UNMAPPED (fresh-review finding).
2978
+ for ln in lines[1:]:
2979
+ s = ln.strip()
2980
+ if s == "---":
2981
+ break
2982
+ if s.startswith("governs:"):
2983
+ rest = s[len("governs:"):].strip()
2984
+ if rest.startswith("[") and rest.endswith("]"):
2985
+ globs = [x.strip().strip("\x27\x22")
2986
+ for x in rest[1:-1].split(",") if x.strip()]
2987
+ in_governs = False
2988
+ elif rest:
2989
+ # bare scalar (`governs: src/**`) -- a plausible authoring shorthand;
2990
+ # dropping it silently would report a misleading "globs match nothing".
2991
+ globs, in_governs = [rest.strip("\x27\x22")], False
2992
+ else:
2993
+ globs, in_governs = [], True
2994
+ continue
2995
+ if in_governs:
2996
+ if s.startswith("- "):
2997
+ globs.append(s[2:].strip().strip("\x27\x22"))
2998
+ elif s and not ln.startswith((" ", "\t")):
2999
+ in_governs = False
3000
+ return globs
3001
+
3002
+
3003
+ def _sd_iso(ct):
3004
+ import datetime as _dt
3005
+ return _dt.datetime.fromtimestamp(ct, _dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
3006
+
3007
+
3008
+ def _sd_last_commit_ct(repo_path, paths):
3009
+ """Newest commit epoch touching any of `paths` (chunked: command lines have limits).
3010
+ None when no commit touches them (untracked)."""
3011
+ newest = None
3012
+ for i in range(0, len(paths), 200):
3013
+ r = subprocess.run(["git", "log", "-1", "--format=%ct", "--"] + paths[i:i + 200],
3014
+ cwd=repo_path, capture_output=True, text=True,
3015
+ encoding="utf-8", errors="replace")
3016
+ out = r.stdout.strip().splitlines()
3017
+ if r.returncode == 0 and out and out[0].strip().isdigit():
3018
+ ct = int(out[0].strip())
3019
+ newest = ct if newest is None else max(newest, ct)
3020
+ return newest
3021
+
3022
+
3023
+ def cmd_spec_drift(args):
3024
+ """Advisory drift report (ADR-005): last commit date of each spec doc vs. the newest
3025
+ commit touching the files its `governs:` globs map to. SPEC_STALE when governed code
3026
+ outran the spec by more than max_lag_days; UNMAPPED when there is no (effective)
3027
+ mapping; UNTRACKED when the spec has no commit date to compare. This command NEVER
3028
+ gates: a stale spec is a prompt for a human conversation, not a blocked pipeline.
3029
+ Exit code 0 always."""
3030
+ ledger = _load(args.ledger)
3031
+ _repo_node(ledger, args.repo)
3032
+ cfg = ledger["config"].get("defaults", {}).get("spec_drift") or {}
3033
+ lag_days = int(args.max_lag_days if args.max_lag_days is not None
3034
+ else cfg.get("max_lag_days", 30))
3035
+ repo_path = _repo_cfg(ledger, args.repo).get("path", ".")
3036
+
3037
+ # The spec surface is fixed by ADR-005: the repo SPEC.md plus every ADR.
3038
+ spec_files = []
3039
+ if os.path.isfile(os.path.join(repo_path, "SPEC.md")):
3040
+ spec_files.append("SPEC.md")
3041
+ adr_dir = os.path.join(repo_path, "docs", "adr")
3042
+ if os.path.isdir(adr_dir):
3043
+ spec_files += sorted("docs/adr/" + f for f in os.listdir(adr_dir)
3044
+ if f.lower().endswith(".md"))
3045
+
3046
+ tracked = []
3047
+ ls = subprocess.run(["git", "ls-files"], cwd=repo_path, capture_output=True,
3048
+ text=True, encoding="utf-8", errors="replace")
3049
+ if ls.returncode == 0:
3050
+ tracked = [l.strip().replace("\\", "/") for l in ls.stdout.splitlines() if l.strip()]
3051
+
3052
+ results = []
3053
+ for spec in spec_files:
3054
+ governs = _sd_governs(os.path.join(repo_path, spec))
3055
+ row = {"file": spec, "governs": governs, "max_lag_days": lag_days}
3056
+ if governs is None:
3057
+ row.update({"verdict": "UNMAPPED", "reason": "no governs: frontmatter"})
3058
+ results.append(row)
3059
+ continue
3060
+ matched = []
3061
+ pats = [_fp_glob_re(g) for g in governs]
3062
+ for f in tracked:
3063
+ if f == spec:
3064
+ continue # a spec governing itself would always read fresh -- excluded
3065
+ if any(p.match(f) for p in pats):
3066
+ matched.append(f)
3067
+ if not matched:
3068
+ # A mapping that matches nothing measures nothing -- same absence, named.
3069
+ row.update({"verdict": "UNMAPPED", "reason": "globs match no tracked files"})
3070
+ results.append(row)
3071
+ continue
3072
+ spec_ct = _sd_last_commit_ct(repo_path, [spec])
3073
+ if spec_ct is None:
3074
+ row.update({"verdict": "UNTRACKED",
3075
+ "reason": "spec has no commit date to compare"})
3076
+ results.append(row)
3077
+ continue
3078
+ newest = _sd_last_commit_ct(repo_path, matched)
3079
+ lag_s = lag_days * 86400
3080
+ row.update({"governed_files": len(matched),
3081
+ "spec_committed_at": _sd_iso(spec_ct),
3082
+ "newest_governed_at": _sd_iso(newest) if newest is not None else None})
3083
+ if newest is not None and newest - spec_ct > lag_s:
3084
+ import datetime as _dt
3085
+ cutoff = _dt.datetime.fromtimestamp(
3086
+ spec_ct + lag_s, _dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%S+0000")
3087
+ newer = set()
3088
+ for i in range(0, len(matched), 200):
3089
+ r = subprocess.run(["git", "log", "--since", cutoff, "--name-only",
3090
+ "--format=", "--"] + matched[i:i + 200],
3091
+ cwd=repo_path, capture_output=True, text=True,
3092
+ encoding="utf-8", errors="replace")
3093
+ if r.returncode == 0:
3094
+ newer |= {l.strip().replace("\\", "/")
3095
+ for l in r.stdout.splitlines() if l.strip()}
3096
+ newer &= set(matched)
3097
+ row.update({"verdict": "SPEC_STALE",
3098
+ "lag_days_actual": round((newest - spec_ct) / 86400.0, 1),
3099
+ "newer_files": sorted(newer)[:20],
3100
+ "newer_files_total": len(newer)})
3101
+ else:
3102
+ row["verdict"] = "CLEAN"
3103
+ results.append(row)
3104
+
3105
+ out = {"repo": args.repo, "max_lag_days": lag_days, "results": results,
3106
+ "advisory": True}
3107
+
3108
+ # Latest-state record so the mirador can surface an advisory row. Advisory data,
3109
+ # not a step in the loop: no step_counter, no gate record, no readiness input.
3110
+ ledger["spec_drift"] = {"repo": args.repo, "at": _now(), "max_lag_days": lag_days,
3111
+ "results": results}
3112
+ _save(args.ledger, ledger)
3113
+
3114
+ if args.json:
3115
+ print(json.dumps(out, indent=2, ensure_ascii=False))
3116
+ else:
3117
+ print("SPEC-DRIFT %s (advisory, lag > %dd):" % (args.repo, lag_days))
3118
+ if not results:
3119
+ print(" no spec documents found (SPEC.md / docs/adr/*.md)")
3120
+ mark = {"SPEC_STALE": "!!", "CLEAN": "ok", "UNMAPPED": "--", "UNTRACKED": "--"}
3121
+ for r_ in results:
3122
+ line = " %s %s: %s" % (mark.get(r_["verdict"], "??"), r_["file"],
3123
+ r_["verdict"])
3124
+ if r_["verdict"] == "SPEC_STALE":
3125
+ line += " -- %d governed file(s) newer, e.g. %s" % (
3126
+ r_["newer_files_total"], ", ".join(r_["newer_files"][:3]))
3127
+ elif "reason" in r_:
3128
+ line += " (%s)" % r_["reason"]
3129
+ print(line)
3130
+ sys.exit(0)
3131
+
3132
+
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
+
2702
3286
  def cmd_escalate(args):
2703
3287
  ledger = _load(args.ledger)
2704
3288
  _repo_node(ledger, args.repo)
@@ -3726,6 +4310,15 @@ def cmd_dashboard(args):
3726
4310
  "snapshots": snapshots,
3727
4311
  "evidence": ev, # receipts (kit 1.50.0): facts with paths + timestamps
3728
4312
  }
4313
+ # fast-path (ADR-003): latest verdict per repo, straight from the ledger. The key exists
4314
+ # ONLY when entries exist: an unconfigured/unused project keeps the exact prior schema
4315
+ # ("absent block = behavior identical" is a measured claim, and an unconditional key was
4316
+ # a schema change that contradicted it -- found by fresh review).
4317
+ if ledger.get("fast_path"):
4318
+ out["fast_path"] = {r: [e for e in ledger["fast_path"] if e.get("repo") == r][-1]
4319
+ for r in {e.get("repo") for e in ledger["fast_path"]}}
4320
+ if ledger.get("spec_drift"):
4321
+ out["spec_drift"] = ledger["spec_drift"]
3729
4322
  if getattr(args, "json", False):
3730
4323
  print(json.dumps(out, indent=2, ensure_ascii=False))
3731
4324
  return
@@ -3897,6 +4490,24 @@ def cmd_readiness(args):
3897
4490
  caps["blocker_critical"], "blocker_critical"))
3898
4491
  if any(not e.get("resolved_at") for e in ledger.get("escalations", [])):
3899
4492
  caps_active.append(("unresolved escalation", caps["escalation"], "escalation"))
4493
+ # AC-FP-06 (ADR-003): an active fast-path run still owes an asserting test. The latest
4494
+ # fast_path entry per repo being ALLOW, with NO measured test execution recorded at or
4495
+ # after it, caps readiness -- reusing the escalation cap value, per the existing cap
4496
+ # mechanics rather than inventing a new one.
4497
+ fp_cfg = ledger["config"].get("defaults", {}).get("fast_path")
4498
+ if isinstance(fp_cfg, dict) and fp_cfg.get("require_asserting_test", True):
4499
+ for _fp_repo in {e.get("repo") for e in ledger.get("fast_path", [])}:
4500
+ entries = [e for e in ledger["fast_path"] if e.get("repo") == _fp_repo]
4501
+ last = entries[-1] if entries else None
4502
+ if not last or last.get("verdict") != "ALLOW":
4503
+ continue
4504
+ node_fp = ledger["repos"].get(_fp_repo, {})
4505
+ tested = any((s.get("tests", {}).get("executed", 0) or 0) > 0
4506
+ and s.get("at", "") >= last.get("at", "")
4507
+ for s in node_fp.get("snapshots", []))
4508
+ if not tested:
4509
+ caps_active.append(("fast-path active in %s without a measured asserting test"
4510
+ % _fp_repo, caps["escalation"], "escalation"))
3900
4511
  if spec_doubts_open:
3901
4512
  caps_active.append((f"{len(spec_doubts_open)} spec-doubt open",
3902
4513
  caps["escalation"], "escalation"))
@@ -6403,6 +7014,31 @@ def build_parser():
6403
7014
  pe.add_argument("--reason", required=True)
6404
7015
  pe.set_defaults(func=cmd_escalate)
6405
7016
 
7017
+ pfp = sub.add_parser("fastpath-eval",
7018
+ help="measured fast-path verdict (ADR-003): ALLOW/DENY from the real diff; --intent records it")
7019
+ pfp.add_argument("--ledger", default="QA-LEDGER.json")
7020
+ pfp.add_argument("--repo", required=True)
7021
+ pfp.add_argument("--base", help="base commit/ref; default merge-base HEAD origin/main (fallback main)")
7022
+ pfp.add_argument("--intent", help="one sentence, what and why; without it the call is a dry-run")
7023
+ pfp.add_argument("--json", action="store_true")
7024
+ pfp.set_defaults(func=cmd_fastpath_eval)
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
+
7034
+ psd = sub.add_parser("spec-drift",
7035
+ help="advisory spec-vs-code drift from git commit dates (ADR-005); never gates, exit 0 always")
7036
+ psd.add_argument("--ledger", default="QA-LEDGER.json")
7037
+ psd.add_argument("--repo", required=True)
7038
+ psd.add_argument("--max-lag-days", type=int, default=None,
7039
+ help="override defaults.spec_drift.max_lag_days (default 30)")
7040
+ psd.add_argument("--json", action="store_true")
7041
+ psd.set_defaults(func=cmd_spec_drift)
6406
7042
  pre = sub.add_parser("resolve-escalation",
6407
7043
  help="close open escalations for a repo (recorded event; "
6408
7044
  "lifts the readiness cap)")