@andresmassello/uscha 1.86.0 → 1.86.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 +2 -2
- package/package.json +1 -1
- package/uscha-kit/.claude/skills/uscha-devloop/qa_ledger.py +62 -27
- package/uscha-kit/.claude/skills/uscha-devloop/uscha_top.py +3 -1
- package/uscha-kit/.claude-plugin/plugin.json +1 -1
- package/uscha-kit/.codex-plugin/plugin.json +1 -1
- package/uscha-kit/README.md +1 -1
- package/uscha-kit/VERSION +1 -1
- package/uscha-kit/install-uscha.py +9 -27
- package/uscha-kit/reports/junit/.top-cases.json +1 -1
- package/uscha-kit/skills/uscha-devloop/qa_ledger.py +62 -27
- package/uscha-kit/skills/uscha-devloop/uscha_top.py +3 -1
- package/uscha-kit/uscha.config.json +1 -1
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.86.
|
|
43
|
+
**Kit v1.86.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
|
|
|
@@ -85,7 +85,7 @@ automatic tool can perform: a human verdict.
|
|
|
85
85
|
from the compiled code: 0.062 measured (12 archetypes) — names, not yet semantics
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
-
**What each arrow is, in the engine (kit 1.86.
|
|
88
|
+
**What each arrow is, in the engine (kit 1.86.1, 52 subcommands, all measured):**
|
|
89
89
|
|
|
90
90
|
| Leg | Subcommands | What it establishes |
|
|
91
91
|
|---|---|---|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andresmassello/uscha",
|
|
3
|
-
"version": "1.86.
|
|
3
|
+
"version": "1.86.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",
|
|
@@ -142,7 +142,7 @@ def _integrity_hash(data):
|
|
|
142
142
|
return hashlib.sha256(blob.encode("utf-8")).hexdigest()
|
|
143
143
|
|
|
144
144
|
|
|
145
|
-
def _load(path):
|
|
145
|
+
def _load(path, what="ledger", flag="--ledger"):
|
|
146
146
|
"""Carga blindada (kit 1.13.0, Topic 34: los recursos compartidos mutables
|
|
147
147
|
incluyen ARCHIVOS). JSON corrupto/truncado = hecho bloqueante con mensaje
|
|
148
148
|
de recuperacion, no un traceback. Si el archivo trae campo integrity
|
|
@@ -153,6 +153,12 @@ def _load(path):
|
|
|
153
153
|
try:
|
|
154
154
|
with open(path, "r", encoding="utf-8") as fh:
|
|
155
155
|
data = json.load(fh)
|
|
156
|
+
except FileNotFoundError:
|
|
157
|
+
# what/flag name the file kind and the flag that points at it (1.86.1 re-judge: a
|
|
158
|
+
# hardcoded "ledger ... --ledger" misled the very first command of a fresh clone,
|
|
159
|
+
# init --config, and rebuild --baseline).
|
|
160
|
+
hint = " -- run the dev loop first, or pass --ledger" if what == "ledger" else f" -- pass {flag}"
|
|
161
|
+
raise SystemExit(f"[qa_ledger] {what} '{path}' not found here{hint}")
|
|
156
162
|
except json.JSONDecodeError as exc:
|
|
157
163
|
raise SystemExit(
|
|
158
164
|
f"[qa_ledger] {path} corrupto (JSON invalido: {exc}). Es un artefacto "
|
|
@@ -820,6 +826,24 @@ def _ac_tags(repo_path, repo_type):
|
|
|
820
826
|
return tags, stale
|
|
821
827
|
|
|
822
828
|
|
|
829
|
+
def _sum_ac_tags(ledger):
|
|
830
|
+
"""One derivation of "how many green/red JUnit testcases does AC-n have, summed across
|
|
831
|
+
every configured repo" -- shared by cmd_readiness (which also keeps up to 8 example case
|
|
832
|
+
receipts per AC and the combined stale-report list) and cmd_top (which only needs
|
|
833
|
+
green/red). One place, so the two readouts of the same fact cannot silently disagree."""
|
|
834
|
+
ac_tags = {}
|
|
835
|
+
stale_reports = []
|
|
836
|
+
for rcfg in (ledger.get("config") or {}).get("repos", []):
|
|
837
|
+
rtags, rstale = _ac_tags(rcfg.get("path", "."), rcfg.get("type", "maven"))
|
|
838
|
+
for cid, v in rtags.items():
|
|
839
|
+
d = ac_tags.setdefault(cid, {"green": 0, "red": 0, "cases": []})
|
|
840
|
+
d["green"] += v["green"]
|
|
841
|
+
d["red"] += v["red"]
|
|
842
|
+
d["cases"] = (d["cases"] + v.get("cases", []))[:8]
|
|
843
|
+
stale_reports.extend(rstale)
|
|
844
|
+
return ac_tags, sorted(set(stale_reports))
|
|
845
|
+
|
|
846
|
+
|
|
823
847
|
def junit_test_count(repo_path, extra_files=None):
|
|
824
848
|
"""JUnit-family XML: `pytest --junitxml=...` (python) and jest-junit /
|
|
825
849
|
vitest --reporter=junit (node). Modern emitters WRAP the root:
|
|
@@ -1832,7 +1856,7 @@ def _validate_log_step_counts(args):
|
|
|
1832
1856
|
|
|
1833
1857
|
|
|
1834
1858
|
def cmd_init(args):
|
|
1835
|
-
cfg = _load(args.config)
|
|
1859
|
+
cfg = _load(args.config, what="config", flag="--config")
|
|
1836
1860
|
_validate_init_config(cfg)
|
|
1837
1861
|
defaults = cfg.get("defaults", {})
|
|
1838
1862
|
ledger = {
|
|
@@ -6892,6 +6916,9 @@ def cmd_facts(args):
|
|
|
6892
6916
|
except OSError as exc:
|
|
6893
6917
|
problems.append((path, 0, "file", "unreadable: %s" % exc, ""))
|
|
6894
6918
|
continue
|
|
6919
|
+
in_table = False
|
|
6920
|
+
table_names = []
|
|
6921
|
+
table_start = 0
|
|
6895
6922
|
for n, line in enumerate(lines, 1):
|
|
6896
6923
|
# an HTML comment is not a published claim -- the first live run flagged a
|
|
6897
6924
|
# section marker (a comment reading "2 Skills") as a drifted count
|
|
@@ -6905,6 +6932,28 @@ def cmd_facts(args):
|
|
|
6905
6932
|
actual = _fact_value(facts, key)
|
|
6906
6933
|
if claimed != actual:
|
|
6907
6934
|
problems.append((path, n, key, claimed, actual))
|
|
6935
|
+
# the parser-surface table (Subcommand/Subcomando header, one `<td class="t">`
|
|
6936
|
+
# row per subcommand) is a claim too, just not a numeric one -- a row can go
|
|
6937
|
+
# missing while the count beside it stays correct (the `top` row did, once).
|
|
6938
|
+
if not in_table:
|
|
6939
|
+
if re.search(r"<th>Sub ?comm?ando?s?</th>", line, re.I):
|
|
6940
|
+
in_table, table_names, table_start = True, [], n
|
|
6941
|
+
continue
|
|
6942
|
+
if "</table>" in line:
|
|
6943
|
+
in_table = False
|
|
6944
|
+
want = set(facts["subcommands"]["list"])
|
|
6945
|
+
got = set(table_names)
|
|
6946
|
+
for name in sorted(want - got):
|
|
6947
|
+
problems.append((path, table_start,
|
|
6948
|
+
"subcommand table: %s" % name,
|
|
6949
|
+
"absent", "present (engine subcommand list)"))
|
|
6950
|
+
for name in sorted(got - want):
|
|
6951
|
+
problems.append((path, table_start,
|
|
6952
|
+
"subcommand table: %s" % name,
|
|
6953
|
+
"present", "absent (not an engine subcommand)"))
|
|
6954
|
+
continue
|
|
6955
|
+
for m in re.finditer(r'<td class="t">([^<]+)</td>', line):
|
|
6956
|
+
table_names.append(m.group(1).strip())
|
|
6908
6957
|
if problems:
|
|
6909
6958
|
print("FACTUAL DRIFT: %d claim(s) disagree with the derived facts"
|
|
6910
6959
|
% len(problems))
|
|
@@ -8133,16 +8182,10 @@ def cmd_top(args):
|
|
|
8133
8182
|
acc_path = args.acceptance or (cfg.get("defaults") or {}).get("acceptance_file")
|
|
8134
8183
|
acc_items, _acc_found = _parse_acceptance_items(acc_path, args.section)
|
|
8135
8184
|
|
|
8136
|
-
# measured evidence: the SAME helper readiness closes criteria with (_ac_tags
|
|
8137
|
-
# across repos exactly as cmd_readiness sums it. Re-deriving it
|
|
8138
|
-
# mirador sin (two derivations of one number, free to disagree).
|
|
8139
|
-
ac_tags =
|
|
8140
|
-
for rcfg in cfg.get("repos", []):
|
|
8141
|
-
rtags, _stale = _ac_tags(rcfg.get("path", "."), rcfg.get("type", "maven"))
|
|
8142
|
-
for cid, v in rtags.items():
|
|
8143
|
-
d = ac_tags.setdefault(cid, {"green": 0, "red": 0})
|
|
8144
|
-
d["green"] += v.get("green", 0)
|
|
8145
|
-
d["red"] += v.get("red", 0)
|
|
8185
|
+
# measured evidence: the SAME helper readiness closes criteria with (_ac_tags via
|
|
8186
|
+
# _sum_ac_tags), summed across repos exactly as cmd_readiness sums it. Re-deriving it
|
|
8187
|
+
# here would be the 1.48.1 mirador sin (two derivations of one number, free to disagree).
|
|
8188
|
+
ac_tags, _stale = _sum_ac_tags(ledger)
|
|
8146
8189
|
|
|
8147
8190
|
# quarantine: UNCURATED observations whose statement literally names an AC id. The link
|
|
8148
8191
|
# is `canonical_match`, a heuristic TEXT match (_match_canonical) -- not a designed
|
|
@@ -8213,7 +8256,10 @@ def cmd_top(args):
|
|
|
8213
8256
|
"cases_pass": (tag or {}).get("green", 0),
|
|
8214
8257
|
"cases_total": (tag or {}).get("green", 0) + (tag or {}).get("red", 0),
|
|
8215
8258
|
"trace": [], # no general AC->implementation map yet (ADR-035/5)
|
|
8216
|
-
|
|
8259
|
+
# red/green evidence outranks the lateral QUARANTINE rung (the state ladder just
|
|
8260
|
+
# above), so an OBS that merely NAMES this AC must not travel into the JSON once
|
|
8261
|
+
# the criterion is already measured -- only a state of QUARANTINE ever carries it.
|
|
8262
|
+
"quarantine_obs": obs if state == "QUARANTINE" else None,
|
|
8217
8263
|
"ac": None, # contract slot with no second honest meaning here
|
|
8218
8264
|
"age_hours": None}) # no first-seen timestamp exists (ADR-035/1)
|
|
8219
8265
|
|
|
@@ -8291,18 +8337,7 @@ def cmd_readiness(args):
|
|
|
8291
8337
|
# cierra solo con >=1 testcase verde taggeado en los reportes JUnit ya
|
|
8292
8338
|
# ingeridos (y 0 rojos). El checkbox es RELATO; el testcase es HECHO.
|
|
8293
8339
|
ac_ids = [i for i in acc_items if i["id"]]
|
|
8294
|
-
ac_tags =
|
|
8295
|
-
stale_reports = []
|
|
8296
|
-
for rcfg in ledger["config"].get("repos", []):
|
|
8297
|
-
rtags, rstale = _ac_tags(rcfg.get("path", "."),
|
|
8298
|
-
rcfg.get("type", "maven"))
|
|
8299
|
-
for cid, v in rtags.items():
|
|
8300
|
-
d = ac_tags.setdefault(cid, {"green": 0, "red": 0, "cases": []})
|
|
8301
|
-
d["green"] += v["green"]
|
|
8302
|
-
d["red"] += v["red"]
|
|
8303
|
-
d["cases"] = (d["cases"] + v.get("cases", []))[:8]
|
|
8304
|
-
stale_reports.extend(rstale)
|
|
8305
|
-
stale_reports = sorted(set(stale_reports))
|
|
8340
|
+
ac_tags, stale_reports = _sum_ac_tags(ledger)
|
|
8306
8341
|
|
|
8307
8342
|
def _ac_closed(cid):
|
|
8308
8343
|
d = ac_tags.get(cid)
|
|
@@ -8827,7 +8862,7 @@ def cmd_rebuild(args):
|
|
|
8827
8862
|
|
|
8828
8863
|
|
|
8829
8864
|
def _rebuild_baseline(args):
|
|
8830
|
-
cfg = _load(args.config)
|
|
8865
|
+
cfg = _load(args.config, what="config", flag="--config")
|
|
8831
8866
|
defaults = cfg.get("defaults", {})
|
|
8832
8867
|
acc_default = args.acceptance or defaults.get("acceptance_file")
|
|
8833
8868
|
tol = (args.coverage_tolerance if args.coverage_tolerance is not None
|
|
@@ -8853,7 +8888,7 @@ def _rebuild_baseline(args):
|
|
|
8853
8888
|
|
|
8854
8889
|
|
|
8855
8890
|
def _rebuild_compare(args):
|
|
8856
|
-
base = _load(args.baseline)
|
|
8891
|
+
base = _load(args.baseline, what="baseline", flag="--baseline")
|
|
8857
8892
|
tol = base.get("coverage_tolerance", DEFAULT_COVERAGE_TOLERANCE)
|
|
8858
8893
|
acc_path = args.acceptance or base.get("acceptance_file")
|
|
8859
8894
|
section = args.section if args.section is not None else base.get("section")
|
|
@@ -27,7 +27,6 @@ import subprocess
|
|
|
27
27
|
import sys
|
|
28
28
|
|
|
29
29
|
DEFAULT_LEDGER = "QA-LEDGER.json"
|
|
30
|
-
DEFAULT_SIZE = (100, 32)
|
|
31
30
|
FALLBACK_SIZE = (100, 32)
|
|
32
31
|
|
|
33
32
|
# Lines the board always spends on chrome: the title, 3 rules, 4 KPI lines, the table
|
|
@@ -257,6 +256,9 @@ def load_state(state_path=None, ledger=DEFAULT_LEDGER, engine=None):
|
|
|
257
256
|
if state_path:
|
|
258
257
|
with open(state_path, "r", encoding="utf-8") as fh:
|
|
259
258
|
return json.load(fh)
|
|
259
|
+
if not os.path.isfile(ledger):
|
|
260
|
+
raise RuntimeError("ledger '%s' not found here -- run the dev loop first, or pass "
|
|
261
|
+
"--ledger" % ledger)
|
|
260
262
|
eng = engine or engine_path()
|
|
261
263
|
if not eng:
|
|
262
264
|
raise RuntimeError("qa_ledger.py not found next to uscha_top.py")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "uscha",
|
|
4
|
-
"version": "1.86.
|
|
4
|
+
"version": "1.86.1",
|
|
5
5
|
"displayName": "Uscha",
|
|
6
6
|
"description": "Spec-driven development for LLM coding agents: 9 skills (discovery, adr-refine, reverse-discovery, characterize, devloop, sysdoc, rubric, mirador, status) + a stdlib measurement engine (qa_ledger.py, 52 subcommands + universal installer + npm/npx router). Facts block, guesses advise; the human approves.",
|
|
7
7
|
"author": {
|
package/uscha-kit/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# uscha-kit
|
|
2
2
|
|
|
3
|
-
**Kit version:** v1.86.
|
|
3
|
+
**Kit version:** v1.86.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`,
|
package/uscha-kit/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
uscha-kit 1.86.
|
|
1
|
+
uscha-kit 1.86.1
|
|
@@ -758,10 +758,12 @@ def _open_best_effort(path):
|
|
|
758
758
|
pass # the renderer already printed the absolute path
|
|
759
759
|
|
|
760
760
|
|
|
761
|
-
def
|
|
762
|
-
"""
|
|
763
|
-
|
|
764
|
-
|
|
761
|
+
def _kit_script_path(skill_dir, filename):
|
|
762
|
+
"""A script inside this kit, in either skill-tree layout (`skills/<skill_dir>/` or
|
|
763
|
+
`.claude/skills/<skill_dir>/`) -- the one resolution every sibling script lookup in this
|
|
764
|
+
file shares, and the same both-layouts precedent `uscha_top.py::engine_path()` uses on
|
|
765
|
+
its own side of the lookup."""
|
|
766
|
+
for rel in (("skills", skill_dir, filename), (".claude", "skills", skill_dir, filename)):
|
|
765
767
|
candidate = KIT_ROOT.joinpath(*rel)
|
|
766
768
|
if candidate.is_file():
|
|
767
769
|
return candidate
|
|
@@ -772,7 +774,7 @@ def cmd_mirador(args):
|
|
|
772
774
|
"""`uscha mirador` — one command to render + open the project's dashboard.
|
|
773
775
|
No paths, no python: the renderer self-resolves its engine/template siblings, and the
|
|
774
776
|
ledger defaults to the QA-LEDGER.json convention in the current directory."""
|
|
775
|
-
render =
|
|
777
|
+
render = _kit_script_path("uscha-mirador", "mirador-render.py")
|
|
776
778
|
if render is None:
|
|
777
779
|
print("[uscha mirador] mirador-render.py not found in the kit", file=sys.stderr)
|
|
778
780
|
raise SystemExit(1)
|
|
@@ -807,26 +809,6 @@ def cmd_mirador(args):
|
|
|
807
809
|
print("\n[uscha mirador] stopped")
|
|
808
810
|
|
|
809
811
|
|
|
810
|
-
def _uscha_top_path():
|
|
811
|
-
"""The `uscha top` renderer inside this kit (either skill-tree layout)."""
|
|
812
|
-
for rel in (("skills", "uscha-devloop", "uscha_top.py"),
|
|
813
|
-
(".claude", "skills", "uscha-devloop", "uscha_top.py")):
|
|
814
|
-
candidate = KIT_ROOT.joinpath(*rel)
|
|
815
|
-
if candidate.is_file():
|
|
816
|
-
return candidate
|
|
817
|
-
return None
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
def _qa_ledger_path():
|
|
821
|
-
"""The engine inside this kit (either skill-tree layout)."""
|
|
822
|
-
for rel in (("skills", "uscha-devloop", "qa_ledger.py"),
|
|
823
|
-
(".claude", "skills", "uscha-devloop", "qa_ledger.py")):
|
|
824
|
-
candidate = KIT_ROOT.joinpath(*rel)
|
|
825
|
-
if candidate.is_file():
|
|
826
|
-
return candidate
|
|
827
|
-
return None
|
|
828
|
-
|
|
829
|
-
|
|
830
812
|
def cmd_top(args):
|
|
831
813
|
"""`uscha top` — the live terminal board of the project's ledger (ADR-031).
|
|
832
814
|
Wired exactly like `mirador`: resolve the sibling script inside the kit and exec it with
|
|
@@ -837,7 +819,7 @@ def cmd_top(args):
|
|
|
837
819
|
"--ledger" % args.ledger, file=sys.stderr)
|
|
838
820
|
raise SystemExit(1)
|
|
839
821
|
if args.json:
|
|
840
|
-
engine =
|
|
822
|
+
engine = _kit_script_path("uscha-devloop", "qa_ledger.py")
|
|
841
823
|
if engine is None:
|
|
842
824
|
print("[uscha top] qa_ledger.py not found in the kit", file=sys.stderr)
|
|
843
825
|
raise SystemExit(1)
|
|
@@ -846,7 +828,7 @@ def cmd_top(args):
|
|
|
846
828
|
if rc:
|
|
847
829
|
raise SystemExit(rc)
|
|
848
830
|
return
|
|
849
|
-
renderer =
|
|
831
|
+
renderer = _kit_script_path("uscha-devloop", "uscha_top.py")
|
|
850
832
|
if renderer is None:
|
|
851
833
|
print("[uscha top] uscha_top.py not found in the kit", file=sys.stderr)
|
|
852
834
|
raise SystemExit(1)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"AC-T-01": true, "AC-T-02": true, "AC-T-03": true, "AC-T-10": true, "AC-T-04": true, "AC-T-05": true, "AC-T-06": true, "AC-T-09": true, "AC-T-24": true, "AC-T-19": true, "AC-T-23": true, "AC-T-21": true, "AC-T-08": true, "AC-T-07": true, "AC-T-18": true, "AC-T-20": true, "AC-T-22": true}
|
|
1
|
+
{"AC-T-01": true, "AC-T-02": true, "AC-T-03": true, "AC-T-10": true, "AC-T-04": true, "AC-T-05": true, "AC-T-06": true, "AC-T-09": true, "AC-T-24": true, "reg-quarantine-obs-null-on-measured": true, "reg-spec-pin-null-outside-worktree": true, "reg-unreachable-repo-named-not-silent": true, "AC-T-19": true, "reg-empty-project-honest": true, "AC-T-23": true, "AC-T-21": true, "AC-T-08": true, "AC-T-07": true, "AC-T-18": true, "AC-T-20": true, "AC-T-22": true, "reg-ledger-not-found": true}
|
|
@@ -142,7 +142,7 @@ def _integrity_hash(data):
|
|
|
142
142
|
return hashlib.sha256(blob.encode("utf-8")).hexdigest()
|
|
143
143
|
|
|
144
144
|
|
|
145
|
-
def _load(path):
|
|
145
|
+
def _load(path, what="ledger", flag="--ledger"):
|
|
146
146
|
"""Carga blindada (kit 1.13.0, Topic 34: los recursos compartidos mutables
|
|
147
147
|
incluyen ARCHIVOS). JSON corrupto/truncado = hecho bloqueante con mensaje
|
|
148
148
|
de recuperacion, no un traceback. Si el archivo trae campo integrity
|
|
@@ -153,6 +153,12 @@ def _load(path):
|
|
|
153
153
|
try:
|
|
154
154
|
with open(path, "r", encoding="utf-8") as fh:
|
|
155
155
|
data = json.load(fh)
|
|
156
|
+
except FileNotFoundError:
|
|
157
|
+
# what/flag name the file kind and the flag that points at it (1.86.1 re-judge: a
|
|
158
|
+
# hardcoded "ledger ... --ledger" misled the very first command of a fresh clone,
|
|
159
|
+
# init --config, and rebuild --baseline).
|
|
160
|
+
hint = " -- run the dev loop first, or pass --ledger" if what == "ledger" else f" -- pass {flag}"
|
|
161
|
+
raise SystemExit(f"[qa_ledger] {what} '{path}' not found here{hint}")
|
|
156
162
|
except json.JSONDecodeError as exc:
|
|
157
163
|
raise SystemExit(
|
|
158
164
|
f"[qa_ledger] {path} corrupto (JSON invalido: {exc}). Es un artefacto "
|
|
@@ -820,6 +826,24 @@ def _ac_tags(repo_path, repo_type):
|
|
|
820
826
|
return tags, stale
|
|
821
827
|
|
|
822
828
|
|
|
829
|
+
def _sum_ac_tags(ledger):
|
|
830
|
+
"""One derivation of "how many green/red JUnit testcases does AC-n have, summed across
|
|
831
|
+
every configured repo" -- shared by cmd_readiness (which also keeps up to 8 example case
|
|
832
|
+
receipts per AC and the combined stale-report list) and cmd_top (which only needs
|
|
833
|
+
green/red). One place, so the two readouts of the same fact cannot silently disagree."""
|
|
834
|
+
ac_tags = {}
|
|
835
|
+
stale_reports = []
|
|
836
|
+
for rcfg in (ledger.get("config") or {}).get("repos", []):
|
|
837
|
+
rtags, rstale = _ac_tags(rcfg.get("path", "."), rcfg.get("type", "maven"))
|
|
838
|
+
for cid, v in rtags.items():
|
|
839
|
+
d = ac_tags.setdefault(cid, {"green": 0, "red": 0, "cases": []})
|
|
840
|
+
d["green"] += v["green"]
|
|
841
|
+
d["red"] += v["red"]
|
|
842
|
+
d["cases"] = (d["cases"] + v.get("cases", []))[:8]
|
|
843
|
+
stale_reports.extend(rstale)
|
|
844
|
+
return ac_tags, sorted(set(stale_reports))
|
|
845
|
+
|
|
846
|
+
|
|
823
847
|
def junit_test_count(repo_path, extra_files=None):
|
|
824
848
|
"""JUnit-family XML: `pytest --junitxml=...` (python) and jest-junit /
|
|
825
849
|
vitest --reporter=junit (node). Modern emitters WRAP the root:
|
|
@@ -1832,7 +1856,7 @@ def _validate_log_step_counts(args):
|
|
|
1832
1856
|
|
|
1833
1857
|
|
|
1834
1858
|
def cmd_init(args):
|
|
1835
|
-
cfg = _load(args.config)
|
|
1859
|
+
cfg = _load(args.config, what="config", flag="--config")
|
|
1836
1860
|
_validate_init_config(cfg)
|
|
1837
1861
|
defaults = cfg.get("defaults", {})
|
|
1838
1862
|
ledger = {
|
|
@@ -6892,6 +6916,9 @@ def cmd_facts(args):
|
|
|
6892
6916
|
except OSError as exc:
|
|
6893
6917
|
problems.append((path, 0, "file", "unreadable: %s" % exc, ""))
|
|
6894
6918
|
continue
|
|
6919
|
+
in_table = False
|
|
6920
|
+
table_names = []
|
|
6921
|
+
table_start = 0
|
|
6895
6922
|
for n, line in enumerate(lines, 1):
|
|
6896
6923
|
# an HTML comment is not a published claim -- the first live run flagged a
|
|
6897
6924
|
# section marker (a comment reading "2 Skills") as a drifted count
|
|
@@ -6905,6 +6932,28 @@ def cmd_facts(args):
|
|
|
6905
6932
|
actual = _fact_value(facts, key)
|
|
6906
6933
|
if claimed != actual:
|
|
6907
6934
|
problems.append((path, n, key, claimed, actual))
|
|
6935
|
+
# the parser-surface table (Subcommand/Subcomando header, one `<td class="t">`
|
|
6936
|
+
# row per subcommand) is a claim too, just not a numeric one -- a row can go
|
|
6937
|
+
# missing while the count beside it stays correct (the `top` row did, once).
|
|
6938
|
+
if not in_table:
|
|
6939
|
+
if re.search(r"<th>Sub ?comm?ando?s?</th>", line, re.I):
|
|
6940
|
+
in_table, table_names, table_start = True, [], n
|
|
6941
|
+
continue
|
|
6942
|
+
if "</table>" in line:
|
|
6943
|
+
in_table = False
|
|
6944
|
+
want = set(facts["subcommands"]["list"])
|
|
6945
|
+
got = set(table_names)
|
|
6946
|
+
for name in sorted(want - got):
|
|
6947
|
+
problems.append((path, table_start,
|
|
6948
|
+
"subcommand table: %s" % name,
|
|
6949
|
+
"absent", "present (engine subcommand list)"))
|
|
6950
|
+
for name in sorted(got - want):
|
|
6951
|
+
problems.append((path, table_start,
|
|
6952
|
+
"subcommand table: %s" % name,
|
|
6953
|
+
"present", "absent (not an engine subcommand)"))
|
|
6954
|
+
continue
|
|
6955
|
+
for m in re.finditer(r'<td class="t">([^<]+)</td>', line):
|
|
6956
|
+
table_names.append(m.group(1).strip())
|
|
6908
6957
|
if problems:
|
|
6909
6958
|
print("FACTUAL DRIFT: %d claim(s) disagree with the derived facts"
|
|
6910
6959
|
% len(problems))
|
|
@@ -8133,16 +8182,10 @@ def cmd_top(args):
|
|
|
8133
8182
|
acc_path = args.acceptance or (cfg.get("defaults") or {}).get("acceptance_file")
|
|
8134
8183
|
acc_items, _acc_found = _parse_acceptance_items(acc_path, args.section)
|
|
8135
8184
|
|
|
8136
|
-
# measured evidence: the SAME helper readiness closes criteria with (_ac_tags
|
|
8137
|
-
# across repos exactly as cmd_readiness sums it. Re-deriving it
|
|
8138
|
-
# mirador sin (two derivations of one number, free to disagree).
|
|
8139
|
-
ac_tags =
|
|
8140
|
-
for rcfg in cfg.get("repos", []):
|
|
8141
|
-
rtags, _stale = _ac_tags(rcfg.get("path", "."), rcfg.get("type", "maven"))
|
|
8142
|
-
for cid, v in rtags.items():
|
|
8143
|
-
d = ac_tags.setdefault(cid, {"green": 0, "red": 0})
|
|
8144
|
-
d["green"] += v.get("green", 0)
|
|
8145
|
-
d["red"] += v.get("red", 0)
|
|
8185
|
+
# measured evidence: the SAME helper readiness closes criteria with (_ac_tags via
|
|
8186
|
+
# _sum_ac_tags), summed across repos exactly as cmd_readiness sums it. Re-deriving it
|
|
8187
|
+
# here would be the 1.48.1 mirador sin (two derivations of one number, free to disagree).
|
|
8188
|
+
ac_tags, _stale = _sum_ac_tags(ledger)
|
|
8146
8189
|
|
|
8147
8190
|
# quarantine: UNCURATED observations whose statement literally names an AC id. The link
|
|
8148
8191
|
# is `canonical_match`, a heuristic TEXT match (_match_canonical) -- not a designed
|
|
@@ -8213,7 +8256,10 @@ def cmd_top(args):
|
|
|
8213
8256
|
"cases_pass": (tag or {}).get("green", 0),
|
|
8214
8257
|
"cases_total": (tag or {}).get("green", 0) + (tag or {}).get("red", 0),
|
|
8215
8258
|
"trace": [], # no general AC->implementation map yet (ADR-035/5)
|
|
8216
|
-
|
|
8259
|
+
# red/green evidence outranks the lateral QUARANTINE rung (the state ladder just
|
|
8260
|
+
# above), so an OBS that merely NAMES this AC must not travel into the JSON once
|
|
8261
|
+
# the criterion is already measured -- only a state of QUARANTINE ever carries it.
|
|
8262
|
+
"quarantine_obs": obs if state == "QUARANTINE" else None,
|
|
8217
8263
|
"ac": None, # contract slot with no second honest meaning here
|
|
8218
8264
|
"age_hours": None}) # no first-seen timestamp exists (ADR-035/1)
|
|
8219
8265
|
|
|
@@ -8291,18 +8337,7 @@ def cmd_readiness(args):
|
|
|
8291
8337
|
# cierra solo con >=1 testcase verde taggeado en los reportes JUnit ya
|
|
8292
8338
|
# ingeridos (y 0 rojos). El checkbox es RELATO; el testcase es HECHO.
|
|
8293
8339
|
ac_ids = [i for i in acc_items if i["id"]]
|
|
8294
|
-
ac_tags =
|
|
8295
|
-
stale_reports = []
|
|
8296
|
-
for rcfg in ledger["config"].get("repos", []):
|
|
8297
|
-
rtags, rstale = _ac_tags(rcfg.get("path", "."),
|
|
8298
|
-
rcfg.get("type", "maven"))
|
|
8299
|
-
for cid, v in rtags.items():
|
|
8300
|
-
d = ac_tags.setdefault(cid, {"green": 0, "red": 0, "cases": []})
|
|
8301
|
-
d["green"] += v["green"]
|
|
8302
|
-
d["red"] += v["red"]
|
|
8303
|
-
d["cases"] = (d["cases"] + v.get("cases", []))[:8]
|
|
8304
|
-
stale_reports.extend(rstale)
|
|
8305
|
-
stale_reports = sorted(set(stale_reports))
|
|
8340
|
+
ac_tags, stale_reports = _sum_ac_tags(ledger)
|
|
8306
8341
|
|
|
8307
8342
|
def _ac_closed(cid):
|
|
8308
8343
|
d = ac_tags.get(cid)
|
|
@@ -8827,7 +8862,7 @@ def cmd_rebuild(args):
|
|
|
8827
8862
|
|
|
8828
8863
|
|
|
8829
8864
|
def _rebuild_baseline(args):
|
|
8830
|
-
cfg = _load(args.config)
|
|
8865
|
+
cfg = _load(args.config, what="config", flag="--config")
|
|
8831
8866
|
defaults = cfg.get("defaults", {})
|
|
8832
8867
|
acc_default = args.acceptance or defaults.get("acceptance_file")
|
|
8833
8868
|
tol = (args.coverage_tolerance if args.coverage_tolerance is not None
|
|
@@ -8853,7 +8888,7 @@ def _rebuild_baseline(args):
|
|
|
8853
8888
|
|
|
8854
8889
|
|
|
8855
8890
|
def _rebuild_compare(args):
|
|
8856
|
-
base = _load(args.baseline)
|
|
8891
|
+
base = _load(args.baseline, what="baseline", flag="--baseline")
|
|
8857
8892
|
tol = base.get("coverage_tolerance", DEFAULT_COVERAGE_TOLERANCE)
|
|
8858
8893
|
acc_path = args.acceptance or base.get("acceptance_file")
|
|
8859
8894
|
section = args.section if args.section is not None else base.get("section")
|
|
@@ -27,7 +27,6 @@ import subprocess
|
|
|
27
27
|
import sys
|
|
28
28
|
|
|
29
29
|
DEFAULT_LEDGER = "QA-LEDGER.json"
|
|
30
|
-
DEFAULT_SIZE = (100, 32)
|
|
31
30
|
FALLBACK_SIZE = (100, 32)
|
|
32
31
|
|
|
33
32
|
# Lines the board always spends on chrome: the title, 3 rules, 4 KPI lines, the table
|
|
@@ -257,6 +256,9 @@ def load_state(state_path=None, ledger=DEFAULT_LEDGER, engine=None):
|
|
|
257
256
|
if state_path:
|
|
258
257
|
with open(state_path, "r", encoding="utf-8") as fh:
|
|
259
258
|
return json.load(fh)
|
|
259
|
+
if not os.path.isfile(ledger):
|
|
260
|
+
raise RuntimeError("ledger '%s' not found here -- run the dev loop first, or pass "
|
|
261
|
+
"--ledger" % ledger)
|
|
260
262
|
eng = engine or engine_path()
|
|
261
263
|
if not eng:
|
|
262
264
|
raise RuntimeError("qa_ledger.py not found next to uscha_top.py")
|