@andresmassello/uscha 1.68.0 → 1.70.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.
@@ -52,11 +52,13 @@ Usage (see `--help` on each subcommand):
52
52
  """
53
53
 
54
54
  import argparse
55
+ import ast
55
56
  import glob
56
57
  import hashlib
57
58
  import json
58
59
  import math
59
60
  import os
61
+ import posixpath
60
62
  import re
61
63
  import shutil
62
64
  import subprocess
@@ -2472,6 +2474,10 @@ def cmd_log_gate(args):
2472
2474
  not-run -> a steps event ONLY, never an iterations record: absence is not
2473
2475
  evidence — it neither reads as clean nor fakes a red (last state stands).
2474
2476
  """
2477
+ # INV-ADVISORY-01 note (ADR-014): --kind is a CLOSED vocabulary (argparse choices), so
2478
+ # an advisory-class dimension (e.g. "semantic") cannot be registered as a gate through
2479
+ # this door at all -- the refusal is structural. The smoke suite measures that the
2480
+ # vocabulary stays closed; widening it to admit an advisory kind is a red build.
2475
2481
  ledger = _load(args.ledger)
2476
2482
  node = _repo_node(ledger, args.repo)
2477
2483
  tool = f"gate:{args.kind}"
@@ -2666,6 +2672,20 @@ def _derive_phase(ledger, name, node, k, qa_order):
2666
2672
  if len(_cu["unjudged"]) > 3 else "")
2667
2673
  + " -- INV-CURATION-01: sin juicio no hay promocion")
2668
2674
  conv = False
2675
+ # CANDIDATE-DELTA gate (ADR-013): same invariant, typed storage. Creating the delta IS
2676
+ # the opt-in; an uncurated OBS blocks exactly like an unjudged .md candidate did.
2677
+ _dl = _delta_state(ledger, name, _cu_cfg.get("path", "."))
2678
+ if _dl is not None:
2679
+ if _dl["malformed"]:
2680
+ reasons.append("CANDIDATE-DELTA invalido: " + "; ".join(_dl["malformed"][:3])
2681
+ + " -- corregir antes de avanzar")
2682
+ conv = False
2683
+ elif _dl["uncurated"]:
2684
+ reasons.append("OBS sin veredicto humano: " + ", ".join(_dl["uncurated"][:3])
2685
+ + (" (+%d)" % (len(_dl["uncurated"]) - 3)
2686
+ if len(_dl["uncurated"]) > 3 else "")
2687
+ + " -- INV-CURATION-01: sin juicio no hay promocion")
2688
+ conv = False
2669
2689
  _cr = _cr_cfg(ledger)
2670
2690
  if _cr and _cr.get("mode") == "final":
2671
2691
  _head = None
@@ -3758,6 +3778,29 @@ def _curation_state(repo_path):
3758
3778
  return state
3759
3779
 
3760
3780
 
3781
+ _SPEC_MARKER_MAX_BYTES = 2 * 1024 * 1024 # a 2MB+ tracked file is not where a spec-id
3782
+ # marker lives; unbounded reads are the T112 lesson
3783
+
3784
+
3785
+ def _scan_spec_markers(repo_path, tracked):
3786
+ """Every `uscha-spec: <id>` marker in the given tracked files, as (file, id) pairs.
3787
+ One scanner shared by roundtrip and fidelity (REUSE-FIRST)."""
3788
+ pat = re.compile(r"uscha-spec:\s*([\w.\-]+)")
3789
+ hits = []
3790
+ for f in tracked:
3791
+ full = os.path.join(repo_path, f.replace("/", os.sep))
3792
+ try:
3793
+ if os.path.getsize(full) > _SPEC_MARKER_MAX_BYTES:
3794
+ continue
3795
+ with open(full, encoding="utf-8", errors="replace") as fh:
3796
+ body = fh.read()
3797
+ except OSError:
3798
+ continue
3799
+ for m in pat.finditer(body):
3800
+ hits.append((f, m.group(1)))
3801
+ return hits
3802
+
3803
+
3761
3804
  def cmd_roundtrip(args):
3762
3805
  """Advisory spec-id coverage (ADR-009 slice 2, v1): which PROMOTED candidates are
3763
3806
  traceable in the code via an embedded `uscha-spec: <candidate>` marker. Coverage by id,
@@ -3779,21 +3822,8 @@ def cmd_roundtrip(args):
3779
3822
  and not l.strip().startswith(CANDIDATE_DIR + "/")
3780
3823
  and os.path.basename(l.strip()) != BEHAVIOR_LEDGER_FILE]
3781
3824
  found = set()
3782
- pat = re.compile(r"uscha-spec:\s*([\w.\-]+)")
3783
- ROUNDTRIP_MAX_BYTES = 2 * 1024 * 1024
3784
- for f in tracked:
3785
- full = os.path.join(repo_path, f)
3786
- try:
3787
- if os.path.getsize(full) > ROUNDTRIP_MAX_BYTES:
3788
- continue # a 2MB+ tracked file is not where a spec-id marker lives; an
3789
- # unbounded full-tree read is the T112 lesson, applied here
3790
- with open(full, encoding="utf-8", errors="replace") as fh:
3791
- body = fh.read()
3792
- except OSError:
3793
- continue
3794
- for m in pat.finditer(body):
3795
- mid = m.group(1)
3796
- found.add(mid if mid.endswith(".md") else mid + ".md")
3825
+ for _f, mid in _scan_spec_markers(repo_path, tracked):
3826
+ found.add(mid if mid.endswith(".md") else mid + ".md")
3797
3827
  covered = [c for c in promoted if c in found]
3798
3828
  missing = [c for c in promoted if c not in found]
3799
3829
  out = {"repo": args.repo, "promoted": len(promoted), "covered": len(covered),
@@ -3862,6 +3892,754 @@ def cmd_curation_check(args):
3862
3892
 
3863
3893
 
3864
3894
 
3895
+ # --------------------------------------------------------------------------- #
3896
+ # CANDIDATE-DELTA (Diamond M1: discovery emits typed observations, verdicts
3897
+ # become ledger objects, fidelity is a vector. ADR-013 / ADR-014.)
3898
+ # --------------------------------------------------------------------------- #
3899
+
3900
+ CANDIDATE_DELTA_FILE = "CANDIDATE-DELTA.json" # under discovery/, machine-canonical
3901
+ CANDIDATE_DELTA_TWIN = "CANDIDATE-DELTA.md" # rendered view, regenerated, never a source
3902
+ CANONICAL_FILE = "CANONICAL.json" # under discovery/: the promoted package
3903
+ ISSUES_DEFERRED_FILE = "ISSUES-DEFERRED.md"
3904
+ OBS_TYPES = ("behavior", "invariant", "contract", "config", "dependency", "decision_trace")
3905
+ EVIDENCE_CLASSES = ("measured", "static", "narrated")
3906
+ # ADR-014 / INV-ADVISORY-01: dimensions an LLM judges can only advise. The QUARANTINE is an
3907
+ # engine invariant: nothing here may ever be registered as a blocking gate.
3908
+ FIDELITY_DIMENSIONS = {
3909
+ "traceability": "measured", "behavior": "measured", "contracts": "measured",
3910
+ "curation_closure": "measured", "unexplained_code": "measured",
3911
+ "semantic": "advisory",
3912
+ }
3913
+ _DELTA_BANNER = ("GENERATED by qa_ledger.py discover (ADR-013). Rendered view of "
3914
+ + CANDIDATE_DELTA_FILE + " -- hand edits are overwritten on regeneration.")
3915
+
3916
+
3917
+ def _delta_seal(observations, repo, path):
3918
+ """The seal covers everything semantically load-bearing that the content-addressed OBS
3919
+ ids do NOT: the full observation set, the repo, and the bound. `path` changes what the
3920
+ delta MEANS (a partial discovery), so it must be sealed -- a hand edit of any of these is
3921
+ a named malformation (fresh-review MEDIUM)."""
3922
+ return _integrity_hash({"observations": observations, "repo": repo, "path": path})
3923
+
3924
+
3925
+ def _obs_id(otype, statement, primary_prov):
3926
+ """Content-addressed: OBS-sha256(type + LF + normalized statement + LF + primary
3927
+ provenance)[:12]. Normalization = lowercase + whitespace collapse. Re-running discovery
3928
+ over unchanged code MUST yield byte-identical ids (AC-DD-04) -- the id is the identity
3929
+ of the observation, not of the run."""
3930
+ norm = re.sub(r"\s+", " ", statement.strip().lower())
3931
+ blob = otype + "\n" + norm + "\n" + primary_prov
3932
+ return "OBS-" + hashlib.sha256(blob.encode("utf-8")).hexdigest()[:12]
3933
+
3934
+
3935
+ def _tracked_files(repo_path):
3936
+ r = subprocess.run(["git", "ls-files"], cwd=repo_path, capture_output=True,
3937
+ text=True, encoding="utf-8", errors="replace")
3938
+ if r.returncode != 0:
3939
+ return None # no git: caller decides what that means
3940
+ return [l.strip() for l in r.stdout.splitlines() if l.strip()]
3941
+
3942
+
3943
+ _STATIC_PY_MAX_BYTES = 2 * 1024 * 1024 # the T112 lesson: never unbounded reads
3944
+
3945
+
3946
+ def _extract_static_py(repo_path, tracked):
3947
+ """v0 static extractors, PYTHON ONLY (ADR-013): public signatures via ast, dependency
3948
+ manifests via requirements*.txt. Deterministic by construction -- if AST/manifest cannot
3949
+ establish it, it is not `static`. Every other stack is UNSUPPORTED: counted and named,
3950
+ never guessed at. Returns (observations, unsupported_count)."""
3951
+ obs, unsupported = [], 0
3952
+ code_exts = (".java", ".kt", ".js", ".ts", ".tsx", ".go", ".rs", ".cs",
3953
+ ".cpp", ".c", ".swift", ".dart", ".rb", ".php")
3954
+ for rel in tracked:
3955
+ low = rel.lower()
3956
+ if low.endswith(code_exts):
3957
+ unsupported += 1
3958
+ continue
3959
+ if not low.endswith(".py"):
3960
+ continue
3961
+ full = os.path.join(repo_path, rel.replace("/", os.sep))
3962
+ try:
3963
+ if os.path.getsize(full) > _STATIC_PY_MAX_BYTES:
3964
+ continue
3965
+ with open(full, encoding="utf-8", errors="replace") as fh:
3966
+ tree = ast.parse(fh.read())
3967
+ except (OSError, SyntaxError):
3968
+ continue # unparseable code yields no static facts
3969
+ for nd in tree.body: # top-level only: the PUBLIC surface
3970
+ if isinstance(nd, (ast.FunctionDef, ast.AsyncFunctionDef)):
3971
+ if nd.name.startswith("_"):
3972
+ continue
3973
+ sig = ", ".join(a.arg for a in nd.args.args)
3974
+ stmt = "%s defines function %s(%s)" % (rel, nd.name, sig)
3975
+ prov = "%s:%d" % (rel, nd.lineno)
3976
+ elif isinstance(nd, ast.ClassDef):
3977
+ if nd.name.startswith("_"):
3978
+ continue
3979
+ stmt = "%s defines class %s" % (rel, nd.name)
3980
+ prov = "%s:%d" % (rel, nd.lineno)
3981
+ else:
3982
+ continue
3983
+ obs.append({"id": _obs_id("contract", stmt, prov), "type": "contract",
3984
+ "statement": stmt, "evidence_class": "static",
3985
+ "provenance": {"files": [prov],
3986
+ "derivation": "AST scan (python ast, top-level defs)",
3987
+ "tool": "qa_ledger-static-py"}})
3988
+ for man in sorted(f for f in tracked
3989
+ if re.match(r"^requirements[^/]*\.txt$", f)):
3990
+ try:
3991
+ with open(os.path.join(repo_path, man), encoding="utf-8",
3992
+ errors="replace") as fh:
3993
+ lines = fh.read().splitlines()
3994
+ except OSError:
3995
+ continue
3996
+ for n, ln in enumerate(lines, 1):
3997
+ s = ln.strip()
3998
+ if not s or s.startswith("#"):
3999
+ continue
4000
+ stmt = "depends on %s (declared in %s)" % (s, man)
4001
+ prov = "%s:%d" % (man, n)
4002
+ obs.append({"id": _obs_id("dependency", stmt, prov), "type": "dependency",
4003
+ "statement": stmt, "evidence_class": "static",
4004
+ "provenance": {"files": [prov],
4005
+ "derivation": "dependency manifest",
4006
+ "tool": "qa_ledger-static-py"}})
4007
+ return obs, unsupported
4008
+
4009
+
4010
+ def _under_bound(rel, bound):
4011
+ return bound is None or rel == bound or rel.startswith(bound + "/")
4012
+
4013
+
4014
+ def _golden_backed_obs(ledger, repo, repo_path, bound=None):
4015
+ """Measured observations: one per approved golden fixture, backed by the LATEST ingested
4016
+ golden-diff gate record (AC-DD-03). Only source: real, ledger-ingested execution. No
4017
+ ingested run -> no measured OBS; a fixture on disk that nothing executed is not evidence."""
4018
+ node = ledger["repos"].get(repo) or {}
4019
+ latest = None
4020
+ for rec in node.get("iterations", []):
4021
+ if rec.get("tool") == "gate:golden-diff":
4022
+ latest = rec
4023
+ if latest is None or latest.get("gated_reported", 1) != 0:
4024
+ return [] # no clean ingested run: nothing measured
4025
+ obs = []
4026
+ for root, dirs, files in os.walk(repo_path):
4027
+ dirs[:] = [d for d in dirs if d not in (".git", "node_modules", ".uscha-worktrees")]
4028
+ for f in sorted(files):
4029
+ if ".approved." not in f:
4030
+ continue
4031
+ rel = os.path.relpath(os.path.join(root, f), repo_path).replace(os.sep, "/")
4032
+ if not _under_bound(rel, bound):
4033
+ continue
4034
+ stmt = "behavior frozen by golden fixture %s matches the approved baseline" % rel
4035
+ obs.append({"id": _obs_id("behavior", stmt, rel), "type": "behavior",
4036
+ "statement": stmt, "evidence_class": "measured",
4037
+ "provenance": {"files": [rel],
4038
+ "derivation": "golden-diff run ingested %s"
4039
+ % latest.get("at"),
4040
+ "tool": "golden-suite"}})
4041
+ return obs
4042
+
4043
+
4044
+ def _load_narrated(path, repo_path):
4045
+ """Strict shape for the skill-supplied narrated observations: a JSON list of
4046
+ {type, statement, files}. The CLASS is the engine's to assign -- an input that declares
4047
+ evidence_class (or an id) is malformation, not a suggestion (ADR-013: the skill
4048
+ narrates, the engine classifies). Returns (observations, errors)."""
4049
+ errors = []
4050
+ try:
4051
+ with open(path, encoding="utf-8-sig") as fh:
4052
+ data = json.load(fh)
4053
+ except (OSError, ValueError) as exc:
4054
+ return [], ["unreadable narrated input: %s" % exc]
4055
+ if not isinstance(data, list):
4056
+ return [], ["narrated input must be a JSON list"]
4057
+ obs = []
4058
+ for i, item in enumerate(data):
4059
+ if not isinstance(item, dict):
4060
+ errors.append("item %d: not an object" % i)
4061
+ continue
4062
+ if "evidence_class" in item or "id" in item:
4063
+ errors.append("item %d: declares %s -- the class and id are the ENGINE's to "
4064
+ "assign; a narrated input cannot self-classify (ADR-013)"
4065
+ % (i, "/".join(k for k in ("evidence_class", "id") if k in item)))
4066
+ continue
4067
+ otype = item.get("type")
4068
+ stmt = item.get("statement")
4069
+ stmt = stmt.strip() if isinstance(stmt, str) else ""
4070
+ files = item.get("files") or []
4071
+ if otype not in OBS_TYPES:
4072
+ errors.append("item %d: type %r (expected %s)" % (i, otype, "|".join(OBS_TYPES)))
4073
+ continue
4074
+ if not stmt:
4075
+ errors.append("item %d: empty statement" % i)
4076
+ continue
4077
+ if not isinstance(files, list) or not all(isinstance(x, str) for x in files):
4078
+ # a non-string ref must be a NAMED refusal, never a TypeError traceback
4079
+ # (fresh-review MEDIUM, reproduced)
4080
+ errors.append("item %d: files must be a list of strings" % i)
4081
+ continue
4082
+ bad = None
4083
+ for ref in files:
4084
+ bad = _resolve_ref(repo_path, ref)
4085
+ if bad:
4086
+ errors.append("item %d: ref %r: %s" % (i, ref, bad))
4087
+ break
4088
+ if bad:
4089
+ continue
4090
+ prov = files[0] if files else "agent-inference"
4091
+ obs.append({"id": _obs_id(otype, stmt, prov), "type": otype,
4092
+ "statement": stmt, "evidence_class": "narrated",
4093
+ "provenance": {"files": files, "derivation": "agent inference",
4094
+ "tool": "skill"}})
4095
+ return obs, errors
4096
+
4097
+
4098
+ def _canonical_ids(repo_path, acceptance_file):
4099
+ """The ids the canonical package answers to today: traceable AC-nn ids from the
4100
+ acceptance file. Match is by ID REFERENCE (ADR-013) -- fuzzy semantic matching is
4101
+ exactly what stays out of scope."""
4102
+ path = acceptance_file if os.path.isabs(acceptance_file) \
4103
+ else os.path.join(repo_path, acceptance_file)
4104
+ if not os.path.isfile(path):
4105
+ return {}
4106
+ ids = {}
4107
+ try:
4108
+ items, _legacy = _parse_acceptance_items(path)
4109
+ except Exception:
4110
+ return {}
4111
+ for it in items or []:
4112
+ if it.get("id"): # normalized "AC-<n>" (numeric ids only)
4113
+ ids[int(it["id"].split("-")[1])] = it["id"]
4114
+ return ids
4115
+
4116
+
4117
+ def _match_canonical(statement, canon_ids):
4118
+ m = re.search(r"(?i)\bAC[-_]?0*(\d+)\b", statement)
4119
+ if m and int(m.group(1)) in canon_ids:
4120
+ return canon_ids[int(m.group(1))]
4121
+ return None
4122
+
4123
+
4124
+ def _render_delta_md(delta, verdicts):
4125
+ lines = ["<!-- %s -->" % _DELTA_BANNER, "",
4126
+ "# CANDIDATE-DELTA (rendered view)", ""]
4127
+ if delta.get("path"):
4128
+ # the bound is the artifact the HUMAN curates from -- a partial discovery must not
4129
+ # read as a complete one, or the shrunk INV-CURATION-01 surface is undisclosed
4130
+ # (fresh-review HIGH). The JSON recorded it; the human-facing view must too.
4131
+ lines += ["> **BOUNDED discovery** — mechanical scans were restricted to "
4132
+ "`%s`. Files outside this path were NOT scanned; this delta is PARTIAL "
4133
+ "by construction." % delta["path"], ""]
4134
+ lines += ["| id | type | class | verdict | statement | provenance |",
4135
+ "|----|------|-------|---------|-----------|------------|"]
4136
+ for o in delta["observations"]:
4137
+ v = verdicts.get(o["id"], "(uncurated)")
4138
+ files = ", ".join(o["provenance"].get("files") or []) or "-"
4139
+ stmt = o["statement"].replace("|", "\\|")
4140
+ lines.append("| %s | %s | %s | %s | %s | %s |"
4141
+ % (o["id"], o["type"], o["evidence_class"], v, stmt, files))
4142
+ lines.append("")
4143
+ return "\n".join(lines)
4144
+
4145
+
4146
+ def _delta_path(repo_path):
4147
+ return os.path.join(repo_path, CANDIDATE_DIR, CANDIDATE_DELTA_FILE)
4148
+
4149
+
4150
+ def _load_delta(repo_path):
4151
+ """Strict loader (ADR-013): malformation is exit-2 class, never a silent degrade. The
4152
+ id of every OBS is RECOMPUTED -- the delta is mechanically derived and never hand-edited,
4153
+ so an id that no longer matches its content is tampering, said as such. Returns
4154
+ (delta, errors); delta None when the file does not exist (feature unused)."""
4155
+ path = _delta_path(repo_path)
4156
+ if not os.path.isfile(path):
4157
+ return None, []
4158
+ errors = []
4159
+ try:
4160
+ with open(path, encoding="utf-8-sig") as fh:
4161
+ delta = json.load(fh)
4162
+ except (OSError, ValueError) as exc:
4163
+ return {}, ["unreadable: %s" % exc]
4164
+ obs = delta.get("observations")
4165
+ if not isinstance(obs, list):
4166
+ return {}, ["no observations list"]
4167
+ seen = set()
4168
+ for i, o in enumerate(obs):
4169
+ if not isinstance(o, dict):
4170
+ errors.append("observation %d: not an object" % i)
4171
+ continue
4172
+ oid = o.get("id")
4173
+ if o.get("type") not in OBS_TYPES:
4174
+ errors.append("%s: type %r" % (oid or i, o.get("type")))
4175
+ if o.get("evidence_class") not in EVIDENCE_CLASSES:
4176
+ errors.append("%s: evidence_class %r" % (oid or i, o.get("evidence_class")))
4177
+ stmt = o.get("statement")
4178
+ prov = o.get("provenance")
4179
+ files = prov.get("files") if isinstance(prov, dict) else None
4180
+ # SHAPE before use (fresh-review HIGH, reproduced): a provenance that is a list, a
4181
+ # non-string statement or a non-string ref must be a NAMED error, never a traceback
4182
+ # -- the crash would take down phase/dashboard, the read-only readouts.
4183
+ if (not isinstance(stmt, str) or not stmt.strip()
4184
+ or not isinstance(prov, dict) or not isinstance(files, list)
4185
+ or not all(isinstance(x, str) for x in files)):
4186
+ errors.append("%s: statement/provenance shape invalid -- the delta is "
4187
+ "derived, never hand-edited" % (oid or i))
4188
+ elif o.get("type") in OBS_TYPES:
4189
+ primary = files[0] if files else "agent-inference"
4190
+ want = _obs_id(o["type"], stmt, primary)
4191
+ if oid != want:
4192
+ errors.append("%s: id does not match its content (recomputed %s) -- the "
4193
+ "delta is derived, never hand-edited" % (oid, want))
4194
+ if oid in seen:
4195
+ errors.append("%s: duplicate id" % oid)
4196
+ seen.add(oid)
4197
+ # the ids cover type+statement+primary provenance; the SEAL covers everything else
4198
+ # (evidence_class, canonical_match, the full ref list) -- without it, one JSON edit
4199
+ # launders narrated inference into measured evidence (fresh-review MEDIUM, reproduced)
4200
+ if not errors:
4201
+ seal = delta.get("_integrity")
4202
+ want = _delta_seal(obs, delta.get("repo"), delta.get("path"))
4203
+ if seal != want:
4204
+ errors.append("integrity seal %s does not match the delta content -- observations, "
4205
+ "repo or path was hand-edited (regenerate via `discover`)"
4206
+ % ("missing" if seal is None else repr(seal)))
4207
+ return delta, errors
4208
+
4209
+
4210
+ def _curation_verdicts(ledger, repo):
4211
+ """Latest verdict per OBS from the append-only ledger records (re-curation supersedes,
4212
+ never deletes -- every superseded record stays retrievable, AC-CU-05)."""
4213
+ verdicts = {}
4214
+ for rec in ledger.get("curation") or []:
4215
+ if rec.get("repo") == repo:
4216
+ verdicts[rec["obs_id"]] = rec["verdict"]
4217
+ return verdicts
4218
+
4219
+
4220
+ def _delta_state(ledger, repo, repo_path):
4221
+ """Everything the gates/readouts need about the delta, or None when unused."""
4222
+ delta, errors = _load_delta(repo_path)
4223
+ if delta is None:
4224
+ return None
4225
+ verdicts = _curation_verdicts(ledger, repo)
4226
+ obs = delta.get("observations") or [] if not errors else []
4227
+ uncurated = [o["id"] for o in obs if o.get("id") not in verdicts]
4228
+ undefined = [o["id"] for o in obs
4229
+ if verdicts.get(o.get("id")) == "undefined"]
4230
+ return {"total": len(obs), "curated": len(obs) - len(uncurated),
4231
+ "uncurated": uncurated, "undefined_open": undefined,
4232
+ "malformed": errors}
4233
+
4234
+
4235
+ def cmd_discover(args):
4236
+ """Emit discovery/CANDIDATE-DELTA.json (ADR-013): typed, content-addressed observations
4237
+ from three strictly separated sources -- measured (ledger-ingested golden runs), static
4238
+ (deterministic extractors, Python-only v0), narrated (skill-supplied inference; the
4239
+ engine classifies and stores, it NEVER calls an LLM). Plus a rendered .md twin."""
4240
+ ledger = _load(args.ledger)
4241
+ _repo_node(ledger, args.repo)
4242
+ repo_path = _scope_path(ledger, args.repo)
4243
+ tracked = _tracked_files(repo_path)
4244
+ if tracked is None:
4245
+ print("[qa_ledger] discover: %s is not a git tree -- discovery derives provenance "
4246
+ "from tracked files and cannot proceed without it." % repo_path,
4247
+ file=sys.stderr)
4248
+ sys.exit(2)
4249
+ bound = None
4250
+ if args.path is not None:
4251
+ # the bound restricts the MECHANICAL scans (static, measured); narrated input stays
4252
+ # the skill's to scope. Field-found before the first run (AC-DD-07): "real, bounded"
4253
+ # is unimplementable without it.
4254
+ raw = args.path.replace("\\", "/").strip()
4255
+ if not raw:
4256
+ # an empty --path is the silent-degrade trap (a wrapper passing an unset var):
4257
+ # falsy would mean "no bound" and quietly scan the whole repo (fresh-review MED)
4258
+ print("[qa_ledger] discover: --path is empty -- omit --path to scan the whole "
4259
+ "repo; an empty bound is not a bound.", file=sys.stderr)
4260
+ sys.exit(2)
4261
+ bound = posixpath.normpath(raw).strip("/") # ./src, src/ , /src -> src
4262
+ if bound in (".", "") or bound == ".." or bound.startswith("../"):
4263
+ print("[qa_ledger] discover: --path %r does not name a subtree inside the repo."
4264
+ % args.path, file=sys.stderr)
4265
+ sys.exit(2)
4266
+ if _gc_rel(os.path.join(repo_path, bound.replace("/", os.sep)), repo_path) is None:
4267
+ print("[qa_ledger] discover: --path %r escapes the repo tree." % args.path,
4268
+ file=sys.stderr)
4269
+ sys.exit(2)
4270
+ tracked = [f for f in tracked if _under_bound(f, bound)]
4271
+ if not tracked:
4272
+ # a typo'd bound silently emitting an empty delta is the silent-degrade trap
4273
+ print("[qa_ledger] discover: --path %r matches no tracked file -- refusing to "
4274
+ "emit an empty delta for a bound that points at nothing." % args.path,
4275
+ file=sys.stderr)
4276
+ sys.exit(2)
4277
+ static_obs, unsupported = _extract_static_py(repo_path, tracked)
4278
+ measured_obs = _golden_backed_obs(ledger, args.repo, repo_path, bound)
4279
+ narrated_obs, nerrs = ([], [])
4280
+ if args.narrated:
4281
+ narrated_obs, nerrs = _load_narrated(args.narrated, repo_path)
4282
+ if nerrs:
4283
+ for e in nerrs:
4284
+ print("[qa_ledger] discover: %s" % e, file=sys.stderr)
4285
+ sys.exit(2) # malformed input: refuse, never degrade
4286
+ by_id = {}
4287
+ for o in measured_obs + static_obs + narrated_obs:
4288
+ by_id.setdefault(o["id"], o) # identical content = the same observation
4289
+ acc = (args.acceptance
4290
+ or ledger.get("config", {}).get("defaults", {}).get("acceptance_file")
4291
+ or "ACCEPTANCE.md")
4292
+ canon = _canonical_ids(repo_path, acc)
4293
+ for o in by_id.values():
4294
+ o["canonical_match"] = _match_canonical(o["statement"], canon)
4295
+ observations = sorted(by_id.values(), key=lambda o: o["id"])
4296
+ delta = {"_generated_by": "qa_ledger.py discover (ADR-013) -- machine-canonical; "
4297
+ "never hand-edit (ids are content-addressed; the seal "
4298
+ "covers observations, repo and path)",
4299
+ "_integrity": _delta_seal(observations, args.repo, bound),
4300
+ "repo": args.repo,
4301
+ **({"path": bound} if bound else {}),
4302
+ "observations": observations,
4303
+ "static_unsupported": {"files": unsupported,
4304
+ "note": "static extractors are Python-only in v0 "
4305
+ "(ADR-013); other stacks report here, "
4306
+ "never guess"}}
4307
+ disc = os.path.join(repo_path, CANDIDATE_DIR)
4308
+ os.makedirs(disc, exist_ok=True)
4309
+ with open(_delta_path(repo_path), "w", encoding="utf-8", newline="\n") as fh:
4310
+ fh.write(json.dumps(delta, indent=2, ensure_ascii=False) + "\n")
4311
+ twin_path = os.path.join(disc, CANDIDATE_DELTA_TWIN)
4312
+ twin = _render_delta_md(delta, _curation_verdicts(ledger, args.repo))
4313
+ prev = None
4314
+ if os.path.isfile(twin_path):
4315
+ try:
4316
+ with open(twin_path, encoding="utf-8-sig") as fh:
4317
+ prev = fh.read()
4318
+ except OSError:
4319
+ prev = None
4320
+ with open(twin_path, "w", encoding="utf-8", newline="\n") as fh:
4321
+ fh.write(twin)
4322
+ if prev is not None and prev != twin:
4323
+ # stderr, not stdout: on this exact path --json must still emit parseable output
4324
+ # (fresh-review MEDIUM, reproduced)
4325
+ print("[qa_ledger] discover: %s differed from the regenerated render -- overwritten "
4326
+ "(the .md twin is a rendered view, never a source; edit verdicts via `curate`)"
4327
+ % CANDIDATE_DELTA_TWIN, file=sys.stderr)
4328
+ counts = {c: sum(1 for o in observations if o["evidence_class"] == c)
4329
+ for c in EVIDENCE_CLASSES}
4330
+ out = {"repo": args.repo, "observations": len(observations), "by_class": counts,
4331
+ "static_unsupported_files": unsupported,
4332
+ "delta": os.path.join(CANDIDATE_DIR, CANDIDATE_DELTA_FILE)}
4333
+ if args.json:
4334
+ print(json.dumps(out, indent=2, ensure_ascii=False))
4335
+ else:
4336
+ print("DISCOVER %s: %d observation(s) (%d measured / %d static / %d narrated) -> %s"
4337
+ % (args.repo, len(observations), counts["measured"], counts["static"],
4338
+ counts["narrated"], out["delta"]))
4339
+ if unsupported:
4340
+ print(" -- %d non-Python source file(s): static extraction UNSUPPORTED in v0 "
4341
+ "(ADR-013) -- reported, not guessed" % unsupported)
4342
+ sys.exit(0)
4343
+
4344
+
4345
+ def cmd_curate(args):
4346
+ """ONE human verdict for ONE observation, recorded as an append-only ledger object
4347
+ (ADR-013). No batch path exists -- and this refusal is the assertion of its absence:
4348
+ curation is the human's judgment applied per-item, never a bulk operation."""
4349
+ if re.search(r"[,\s*]", args.obs) or args.obs.lower() in ("all", "*"):
4350
+ print("[qa_ledger] curate: %r -- one OBS, one human verdict. A batch-accept path "
4351
+ "does not exist and will not (ADR-013, INV-CURATION-01)." % args.obs,
4352
+ file=sys.stderr)
4353
+ sys.exit(2)
4354
+ ledger = _load(args.ledger)
4355
+ _repo_node(ledger, args.repo)
4356
+ repo_path = _scope_path(ledger, args.repo)
4357
+ delta, errors = _load_delta(repo_path)
4358
+ if delta is None:
4359
+ print("[qa_ledger] curate: no %s -- run `discover` first."
4360
+ % os.path.join(CANDIDATE_DIR, CANDIDATE_DELTA_FILE), file=sys.stderr)
4361
+ sys.exit(2)
4362
+ if errors:
4363
+ for e in errors:
4364
+ print("[qa_ledger] curate: delta malformed: %s" % e, file=sys.stderr)
4365
+ sys.exit(2)
4366
+ known = {o["id"] for o in delta.get("observations") or []}
4367
+ if args.obs not in known:
4368
+ print("[qa_ledger] curate: %s is not in the current delta -- a verdict must judge "
4369
+ "a real observation." % args.obs, file=sys.stderr)
4370
+ sys.exit(2)
4371
+ prev = _curation_verdicts(ledger, args.repo).get(args.obs)
4372
+ human = args.human or os.environ.get("USERNAME") or os.environ.get("USER") or "unknown"
4373
+ rec = {"obs_id": args.obs, "verdict": args.verdict, "human": human,
4374
+ "at": _now(), "note": args.note, "repo": args.repo}
4375
+ ledger.setdefault("curation", []).append(rec)
4376
+ _save(args.ledger, ledger)
4377
+ if prev and prev != args.verdict:
4378
+ print("[qa_ledger] curate: %s = %s (supersedes %r -- the earlier record stays; "
4379
+ "append-only, never deleted)" % (args.obs, args.verdict, prev))
4380
+ else:
4381
+ print("[qa_ledger] curate: %s = %s (by %s)" % (args.obs, args.verdict, human))
4382
+ sys.exit(0)
4383
+
4384
+
4385
+ def cmd_promote(args):
4386
+ """Move ONLY preserve-verdict observations into the canonical package, with
4387
+ `derived_from` lineage (ADR-013). fix -> a work item in ISSUES-DEFERRED.md, never
4388
+ canonical. undefined -> stays open in the readouts. ANY uncurated OBS -> hard refusal
4389
+ naming the ids; nothing moves (INV-CURATION-01, fail-closed)."""
4390
+ ledger = _load(args.ledger)
4391
+ _repo_node(ledger, args.repo)
4392
+ repo_path = _scope_path(ledger, args.repo)
4393
+ delta, errors = _load_delta(repo_path)
4394
+ if delta is None:
4395
+ print("[qa_ledger] promote: no %s -- run `discover` first."
4396
+ % os.path.join(CANDIDATE_DIR, CANDIDATE_DELTA_FILE), file=sys.stderr)
4397
+ sys.exit(2)
4398
+ if errors:
4399
+ for e in errors:
4400
+ print("[qa_ledger] promote: delta malformed: %s" % e, file=sys.stderr)
4401
+ sys.exit(2)
4402
+ obs = delta.get("observations") or []
4403
+ verdicts = _curation_verdicts(ledger, args.repo)
4404
+ uncurated = [o["id"] for o in obs if o["id"] not in verdicts]
4405
+ if uncurated:
4406
+ print("[qa_ledger] promote: REFUSED -- %d observation(s) without a human verdict: %s"
4407
+ % (len(uncurated), ", ".join(uncurated[:5])
4408
+ + (" (+%d)" % (len(uncurated) - 5) if len(uncurated) > 5 else "")),
4409
+ file=sys.stderr)
4410
+ print(" INV-CURATION-01: nothing promotes unjudged. Curate each with "
4411
+ "`curate --obs <id> --verdict preserve|fix|undefined`.", file=sys.stderr)
4412
+ sys.exit(1)
4413
+ canon_path = os.path.join(repo_path, CANDIDATE_DIR, CANONICAL_FILE)
4414
+ canonical = {"_generated_by": "qa_ledger.py promote (ADR-013) -- items carry "
4415
+ "derived_from lineage to their OBS", "items": []}
4416
+ if os.path.isfile(canon_path):
4417
+ try:
4418
+ with open(canon_path, encoding="utf-8-sig") as fh:
4419
+ canonical = json.load(fh)
4420
+ except (OSError, ValueError) as exc:
4421
+ print("[qa_ledger] promote: %s unreadable: %s" % (CANONICAL_FILE, exc),
4422
+ file=sys.stderr)
4423
+ sys.exit(2)
4424
+ have = {it.get("derived_from") for it in canonical.get("items") or []}
4425
+ promoted, fixes, undefined_open = [], [], []
4426
+ for o in obs:
4427
+ v = verdicts[o["id"]]
4428
+ if v == "preserve":
4429
+ if o["id"] not in have:
4430
+ canonical.setdefault("items", []).append(
4431
+ {"statement": o["statement"], "type": o["type"],
4432
+ "evidence_class": o["evidence_class"],
4433
+ "provenance": o["provenance"], "derived_from": o["id"]})
4434
+ promoted.append(o["id"])
4435
+ elif v == "fix":
4436
+ fixes.append(o)
4437
+ else:
4438
+ undefined_open.append(o["id"])
4439
+ canonical["items"] = sorted(canonical.get("items") or [],
4440
+ key=lambda it: it.get("derived_from") or "")
4441
+ with open(canon_path, "w", encoding="utf-8", newline="\n") as fh:
4442
+ fh.write(json.dumps(canonical, indent=2, ensure_ascii=False) + "\n")
4443
+ new_fix = []
4444
+ if fixes:
4445
+ dpath = os.path.join(repo_path, ISSUES_DEFERRED_FILE)
4446
+ existing = ""
4447
+ if os.path.isfile(dpath):
4448
+ with open(dpath, encoding="utf-8-sig", errors="replace") as fh:
4449
+ existing = fh.read()
4450
+ add = [o for o in fixes if o["id"] not in existing]
4451
+ if add:
4452
+ with open(dpath, "a", encoding="utf-8", newline="\n") as fh:
4453
+ if existing and not existing.endswith("\n"):
4454
+ fh.write("\n")
4455
+ for o in add:
4456
+ fh.write("- [ ] %s (curated `fix`): %s -- observed behavior the human "
4457
+ "ruled a defect; NEVER canonical (ADR-013)\n"
4458
+ % (o["id"], o["statement"]))
4459
+ new_fix = [o["id"] for o in add]
4460
+ ledger["candidate_delta"] = {"repo": args.repo, "total": len(obs),
4461
+ "curated": len(obs),
4462
+ "undefined_open": undefined_open,
4463
+ "canonical_items": len(canonical["items"]),
4464
+ "at": _now()}
4465
+ _save(args.ledger, ledger)
4466
+ out = {"repo": args.repo, "promoted": promoted, "fix_deferred": new_fix,
4467
+ "undefined_open": undefined_open,
4468
+ "canonical": os.path.join(CANDIDATE_DIR, CANONICAL_FILE)}
4469
+ if args.json:
4470
+ print(json.dumps(out, indent=2, ensure_ascii=False))
4471
+ else:
4472
+ print("PROMOTE %s: %d promoted, %d fix -> %s, %d undefined OPEN"
4473
+ % (args.repo, len(promoted), len(new_fix), ISSUES_DEFERRED_FILE,
4474
+ len(undefined_open)))
4475
+ for oid in undefined_open:
4476
+ print(" .. %s: undefined -- stays open and visible until re-curated" % oid)
4477
+ sys.exit(0)
4478
+
4479
+
4480
+ def _fid_dim(value, provenance, **extra):
4481
+ d = {"value": value, "provenance": provenance}
4482
+ d.update(extra)
4483
+ return d
4484
+
4485
+
4486
+ def cmd_fidelity(args):
4487
+ """The fidelity VECTOR (ADR-014): five independently measured dimensions, each with its
4488
+ own provenance, plus the advisory quarantine. Deterministic: same inputs, same numbers,
4489
+ no LLM anywhere in the measured path. Advisory dimensions can NEVER gate -- attempting
4490
+ to configure one as blocking is an engine refusal, not a configuration
4491
+ (INV-ADVISORY-01)."""
4492
+ ledger = _load(args.ledger)
4493
+ node = _repo_node(ledger, args.repo)
4494
+ repo_path = _scope_path(ledger, args.repo)
4495
+ cfg = {}
4496
+ if os.path.isfile(args.config):
4497
+ try:
4498
+ with open(args.config, encoding="utf-8-sig") as fh:
4499
+ cfg = json.load(fh)
4500
+ except (OSError, ValueError) as exc:
4501
+ # a config that cannot be parsed cannot declare gates -- swallowing the error
4502
+ # would DISABLE the INV-ADVISORY-01 refusal on a syntax slip (fresh-review
4503
+ # HIGH, reproduced). Malformation is exit 2, never a silent degrade.
4504
+ print("[qa_ledger] fidelity: %s unreadable: %s -- refusing to guess what it "
4505
+ "declares." % (args.config, exc), file=sys.stderr)
4506
+ sys.exit(2)
4507
+ declared = ((cfg.get("defaults") or {}).get("fidelity") or {}).get("gate") or []
4508
+ for dim in (ledger.get("config", {}).get("defaults", {}).get("fidelity")
4509
+ or {}).get("gate") or []:
4510
+ if dim not in declared: # both surfaces checked -- no silent path
4511
+ declared.append(dim)
4512
+ for dim in declared:
4513
+ cls = FIDELITY_DIMENSIONS.get(dim)
4514
+ if cls is None:
4515
+ print("[qa_ledger] fidelity: %r is not a dimension (%s)"
4516
+ % (dim, ", ".join(sorted(FIDELITY_DIMENSIONS))), file=sys.stderr)
4517
+ sys.exit(2)
4518
+ if cls == "advisory":
4519
+ print("[qa_ledger] fidelity: REFUSED -- %r is an ADVISORY-class dimension and "
4520
+ "can never be registered as blocking. This is an engine invariant "
4521
+ "(INV-ADVISORY-01, ADR-014), not a configuration." % dim, file=sys.stderr)
4522
+ sys.exit(2)
4523
+ print("[qa_ledger] fidelity: gating on measured dimension %r is not implemented "
4524
+ "in v0 (ADR-014) -- declared but unenforceable is a silent lie; remove it "
4525
+ "or wait for the milestone that wires it." % dim, file=sys.stderr)
4526
+ sys.exit(2)
4527
+ delta, derrs = _load_delta(repo_path)
4528
+ if derrs:
4529
+ # same posture as curate/promote: a delta that EXISTS but cannot be validated is
4530
+ # exit 2, and "no delta" must never be the label for it (fresh-review MEDIUM)
4531
+ for e in derrs:
4532
+ print("[qa_ledger] fidelity: delta malformed: %s" % e, file=sys.stderr)
4533
+ sys.exit(2)
4534
+ obs = (delta.get("observations") or []) if delta else []
4535
+ verdicts = _curation_verdicts(ledger, args.repo)
4536
+ dims = {}
4537
+ # traceability: canonical items reachable in code via the uscha-spec id machinery
4538
+ canon_items = []
4539
+ canon_path = os.path.join(repo_path, CANDIDATE_DIR, CANONICAL_FILE)
4540
+ if os.path.isfile(canon_path):
4541
+ try:
4542
+ with open(canon_path, encoding="utf-8-sig") as fh:
4543
+ canon_items = json.load(fh).get("items") or []
4544
+ except (OSError, ValueError):
4545
+ canon_items = []
4546
+ tracked = _tracked_files(repo_path) or []
4547
+ marked, marker_files = set(), set()
4548
+ for f, mid in _scan_spec_markers(repo_path, tracked):
4549
+ marked.add(mid)
4550
+ marker_files.add(f)
4551
+ if canon_items:
4552
+ traced = sum(1 for it in canon_items if (it.get("derived_from") or "") in marked)
4553
+ dims["traceability"] = _fid_dim(round(traced / len(canon_items), 4),
4554
+ "uscha-spec marker scan over %d git-tracked "
4555
+ "files vs %d canonical item(s)"
4556
+ % (len(tracked), len(canon_items)))
4557
+ else:
4558
+ dims["traceability"] = _fid_dim(None, "UNMEASURED: no canonical items promoted yet")
4559
+ # behavior: the latest ingested golden-diff gate verdict
4560
+ latest_g = None
4561
+ for rec in node.get("iterations", []):
4562
+ if rec.get("tool") == "gate:golden-diff":
4563
+ latest_g = rec
4564
+ if latest_g is not None:
4565
+ ok = latest_g.get("gated_reported", 1) == 0
4566
+ prov = "gate:golden-diff record ingested %s (iteration %s)" % (
4567
+ latest_g.get("at"), latest_g.get("iteration"))
4568
+ cr = _cr_latest(ledger, args.repo)
4569
+ if cr and cr.get("status") == "GREEN":
4570
+ prov += "; clean-room GREEN at %s" % (cr.get("ref") or "?")[:12]
4571
+ dims["behavior"] = _fid_dim(1.0 if ok else 0.0, prov)
4572
+ else:
4573
+ dims["behavior"] = _fid_dim(None, "UNMEASURED: no golden-diff run ingested "
4574
+ "(log-gate --kind golden-diff)")
4575
+ # contracts: canonical static items still derivable from the code RIGHT NOW
4576
+ static_canon = [it for it in canon_items if it.get("evidence_class") == "static"]
4577
+ if static_canon:
4578
+ cur_static, _u = _extract_static_py(repo_path, tracked)
4579
+ cur_ids = {o["id"] for o in cur_static}
4580
+ okc = sum(1 for it in static_canon if it.get("derived_from") in cur_ids)
4581
+ dims["contracts"] = _fid_dim(round(okc / len(static_canon), 4),
4582
+ "re-ran static extractors; %d/%d canonical static "
4583
+ "item(s) still derive from the code"
4584
+ % (okc, len(static_canon)))
4585
+ else:
4586
+ dims["contracts"] = _fid_dim(None, "UNMEASURED: no static-class canonical items")
4587
+ # curation_closure: curated OBS / total OBS in the active delta
4588
+ if obs:
4589
+ cur = sum(1 for o in obs if o["id"] in verdicts)
4590
+ dims["curation_closure"] = _fid_dim(round(cur / len(obs), 4),
4591
+ "%d/%d OBS in %s carry a ledger verdict"
4592
+ % (cur, len(obs), CANDIDATE_DELTA_FILE))
4593
+ else:
4594
+ dims["curation_closure"] = _fid_dim(
4595
+ None, "UNMEASURED: no delta" if delta is None
4596
+ else "no observations in the delta")
4597
+ # unexplained_code: v0 deliberately crude -- unit = source FILE (ADR-014: crude and
4598
+ # honest beats fine-grained and narrated; over-fires on monoliths BY DESIGN)
4599
+ rtype = (_repo_cfg(ledger, args.repo).get("type")
4600
+ if args.repo != "integration" else None)
4601
+ src_exts = (".py", ".java", ".kt", ".js", ".ts", ".tsx", ".go", ".rs", ".cs",
4602
+ ".cpp", ".c", ".swift", ".dart", ".rb", ".php")
4603
+ prod = [f for f in tracked
4604
+ if f.lower().endswith(src_exts)
4605
+ and not f.startswith(CANDIDATE_DIR + "/")
4606
+ and not _is_test_path(f, rtype)]
4607
+ lineage = set(marker_files) # a file CARRYING a spec marker is explained
4608
+ for o in obs:
4609
+ if verdicts.get(o["id"]) == "preserve":
4610
+ for ref in o["provenance"].get("files") or []:
4611
+ lineage.add(ref.split(":")[0].split("#")[0])
4612
+ for it in canon_items:
4613
+ for ref in (it.get("provenance") or {}).get("files") or []:
4614
+ lineage.add(ref.split(":")[0].split("#")[0])
4615
+ if prod:
4616
+ unex = [f for f in prod if f not in lineage]
4617
+ dims["unexplained_code"] = _fid_dim(
4618
+ round(len(unex) / len(prod), 4),
4619
+ "%d/%d tracked prod source file(s) with no path to a canonical item or "
4620
+ "preserved OBS (v0 granularity: FILE)" % (len(unex), len(prod)),
4621
+ files=unex[:10] + (["(+%d)" % (len(unex) - 10)] if len(unex) > 10 else []))
4622
+ else:
4623
+ dims["unexplained_code"] = _fid_dim(None, "UNMEASURED: no tracked prod source files")
4624
+ dims["semantic"] = _fid_dim(None, "not wired: an LLM-judged comparison enters as "
4625
+ "advisory only and can NEVER gate (INV-ADVISORY-01)")
4626
+ out = {"repo": args.repo,
4627
+ "dimensions": {k: dict(dims[k], **{"class": FIDELITY_DIMENSIONS[k]})
4628
+ for k in ("traceability", "behavior", "contracts",
4629
+ "curation_closure", "unexplained_code", "semantic")}}
4630
+ ledger["fidelity"] = dict(out, at=_now())
4631
+ _save(args.ledger, ledger)
4632
+ if args.json:
4633
+ print(json.dumps(out, indent=2, ensure_ascii=False))
4634
+ else:
4635
+ print("FIDELITY %s (vector -- no blend; each number stands on its own evidence):"
4636
+ % args.repo)
4637
+ for k, d in out["dimensions"].items():
4638
+ val = "UNMEASURED" if d["value"] is None else "%.2f" % d["value"]
4639
+ print(" %-17s %-10s [%s] %s" % (k, val, d["class"], d["provenance"]))
4640
+ sys.exit(0)
4641
+
4642
+
3865
4643
  # --------------------------------------------------------------------------- #
3866
4644
  # facts (T0 / SYSTEM-FACTS: public claims become compiled artifacts of repo
3867
4645
  # facts -- Diamond applied to Diamond. ADR-012.)
@@ -5049,6 +5827,20 @@ def cmd_dashboard(args):
5049
5827
  for r in {e.get("repo") for e in ledger[CLEAN_ROOM_KEY]}}
5050
5828
  if ledger.get("roundtrip"):
5051
5829
  out["roundtrip"] = ledger["roundtrip"]
5830
+ # AC-CU-04: undefined verdicts stay OPEN and visible in the readouts -- derived live
5831
+ # from delta + curation records per repo (conditional key, the roundtrip pattern)
5832
+ _dl_all = {}
5833
+ for _rn in ledger["repos"]:
5834
+ try:
5835
+ _dls = _delta_state(ledger, _rn, _scope_path(ledger, _rn))
5836
+ except SystemExit:
5837
+ _dls = None
5838
+ if _dls is not None:
5839
+ _dl_all[_rn] = _dls
5840
+ if _dl_all:
5841
+ out["candidate_delta"] = _dl_all
5842
+ if ledger.get("fidelity"):
5843
+ out["fidelity"] = ledger["fidelity"]
5052
5844
  if getattr(args, "json", False):
5053
5845
  print(json.dumps(out, indent=2, ensure_ascii=False))
5054
5846
  return
@@ -7846,6 +8638,58 @@ def build_parser():
7846
8638
  prt.add_argument("--json", action="store_true")
7847
8639
  prt.set_defaults(func=cmd_roundtrip)
7848
8640
 
8641
+ pdd = sub.add_parser(
8642
+ "discover",
8643
+ help="emit discovery/CANDIDATE-DELTA.json: typed observations with content-addressed "
8644
+ "OBS ids -- measured/static/narrated, strictly classified (ADR-013)")
8645
+ pdd.add_argument("--ledger", default="QA-LEDGER.json")
8646
+ pdd.add_argument("--repo", required=True)
8647
+ pdd.add_argument("--narrated", default=None,
8648
+ help="JSON list of skill-supplied observations {type, statement, files}; "
8649
+ "the engine classifies them narrated -- it never calls an LLM")
8650
+ pdd.add_argument("--path", default=None,
8651
+ help="bound the mechanical scans to one subtree/file (repo-relative); "
8652
+ "a bound matching nothing is a refusal, and it is recorded in "
8653
+ "the delta")
8654
+ pdd.add_argument("--acceptance", default=None,
8655
+ help="acceptance file for canonical_match (default: config "
8656
+ "defaults.acceptance_file, else ACCEPTANCE.md)")
8657
+ pdd.add_argument("--json", action="store_true")
8658
+ pdd.set_defaults(func=cmd_discover)
8659
+
8660
+ pcv = sub.add_parser(
8661
+ "curate",
8662
+ help="record ONE human verdict (preserve|fix|undefined) for ONE observation as an "
8663
+ "append-only ledger object; no batch path exists (ADR-013)")
8664
+ pcv.add_argument("--ledger", default="QA-LEDGER.json")
8665
+ pcv.add_argument("--repo", required=True)
8666
+ pcv.add_argument("--obs", required=True, help="a single OBS id from the delta")
8667
+ pcv.add_argument("--verdict", required=True, choices=("preserve", "fix", "undefined"))
8668
+ pcv.add_argument("--note", default=None)
8669
+ pcv.add_argument("--human", default=None,
8670
+ help="who judged (default: the OS user)")
8671
+ pcv.set_defaults(func=cmd_curate)
8672
+
8673
+ ppr = sub.add_parser(
8674
+ "promote",
8675
+ help="move preserve-verdict observations into discovery/CANONICAL.json with "
8676
+ "derived_from lineage; refuses over ANY uncurated OBS (INV-CURATION-01)")
8677
+ ppr.add_argument("--ledger", default="QA-LEDGER.json")
8678
+ ppr.add_argument("--repo", required=True)
8679
+ ppr.add_argument("--json", action="store_true")
8680
+ ppr.set_defaults(func=cmd_promote)
8681
+
8682
+ pfv = sub.add_parser(
8683
+ "fidelity",
8684
+ help="the fidelity vector: 5 measured dimensions + advisory quarantine; an advisory "
8685
+ "dimension can NEVER gate (ADR-014, INV-ADVISORY-01)")
8686
+ pfv.add_argument("--ledger", default="QA-LEDGER.json")
8687
+ pfv.add_argument("--repo", required=True)
8688
+ pfv.add_argument("--config", default="uscha.config.json",
8689
+ help="checked for defaults.fidelity.gate -- advisory there is a refusal")
8690
+ pfv.add_argument("--json", action="store_true")
8691
+ pfv.set_defaults(func=cmd_fidelity)
8692
+
7849
8693
  pcr = sub.add_parser("cleanroom",
7850
8694
  help="run a command against a CLEAN checkout of one commit in a throwaway worktree (ADR-008)")
7851
8695
  pcr.add_argument("--ledger", default="QA-LEDGER.json")