@andresmassello/uscha 1.86.1 → 1.87.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +1 -1
- package/uscha-kit/.claude/skills/uscha-devloop/SKILL.md +4 -1
- package/uscha-kit/.claude/skills/uscha-devloop/qa_ledger.py +100 -29
- 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/reports/junit/.fa-cases.json +1 -0
- package/uscha-kit/skills/uscha-devloop/SKILL.md +4 -1
- package/uscha-kit/skills/uscha-devloop/qa_ledger.py +100 -29
- package/uscha-kit/templates/scripts/uscha_progress.py +8 -2
- 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.
|
|
43
|
+
**Kit v1.87.0** <!-- uscha:version --> · [uscha.dev](https://uscha.dev) ·
|
|
44
44
|
[changelog](https://github.com/andresmassello/uscha/blob/main/uscha-kit/CHANGELOG.md)
|
|
45
45
|
(the per-release changelogs live in the repo, not in the npm tarball)
|
|
46
46
|
|
|
@@ -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.
|
|
88
|
+
**What each arrow is, in the engine (kit 1.87.0, 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.
|
|
3
|
+
"version": "1.87.0",
|
|
4
4
|
"description": "Spec-driven development for LLM coding agents: 9 skills + a stdlib evidence engine. Facts block, guesses advise; the human approves.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Andres Massello",
|
|
@@ -535,7 +535,10 @@ once logged it caps readiness ≤65 and blocks convergence until resolved with
|
|
|
535
535
|
criterion carries a stable ID: `- [ ] AC-01 — when X then Y`. A criterion counts as
|
|
536
536
|
CLOSED only when ≥1 GREEN testcase whose name carries the tag (`test_ac1_x`,
|
|
537
537
|
`testAC01X`, `"AC-01: ..."` — IDs normalize by number, `AC-01 == AC_1 == ac1`) exists
|
|
538
|
-
in the ingested JUnit reports AND no tagged testcase is red.
|
|
538
|
+
in the ingested JUnit reports AND no tagged testcase is red. Since kit 1.87.0 (ADR-036)
|
|
539
|
+
a FAMILY prefix is read the same way: `- [ ] AC-BC-07 — ...` closes on `AC-BC-07_x`,
|
|
540
|
+
`test_ac_bc_7_y` or `AC_BC_7` (normalized to `AC-BC-7`; the family needs a separator on
|
|
541
|
+
both sides — camelCase `testACBC07` is NOT a tag, and `AC-7-x` is still the bare `AC-7`). The checkbox is the
|
|
539
542
|
NARRATIVE; the testcase is the FACT — a checked box without a green tagged test shows
|
|
540
543
|
up as `narrated_only` and does NOT close (measured beats narrated, per criterion).
|
|
541
544
|
A JUnit report older than the repo's source code is treated as STALE (the code changed
|
|
@@ -764,11 +764,43 @@ def _test_evidence_provenance(repo_path, repo_type):
|
|
|
764
764
|
_AC_TAG = re.compile(
|
|
765
765
|
r"(?:(?<![A-Za-z0-9])[Aa][Cc]|(?<=[a-z])AC)[-_]?0*(\d+)(?!\d)")
|
|
766
766
|
|
|
767
|
+
# kit 1.87.0 (ADR-036): the FAMILY grammar 'AC-<FAMILY>-<n>' (AC-BC-07, AC-T-24,
|
|
768
|
+
# ac_dd_3). Same explicit boundaries as the bare form, plus a MANDATORY separator
|
|
769
|
+
# on BOTH sides of the family — 'AC-BC-07', 'ac_bc_7', 'AC_T_1'. camelCase families
|
|
770
|
+
# are deliberately NOT supported: 'testACBC07' has no honest split into family +
|
|
771
|
+
# number and stays unmatched.
|
|
772
|
+
# Kept as its OWN pattern so the bare pattern above (and every tag it already
|
|
773
|
+
# produced) stays byte-identical. The two are disjoint by construction: a family
|
|
774
|
+
# must start with a LETTER, so 'AC-01' can never match this one, and the digits of
|
|
775
|
+
# 'AC-BC-07' do not follow 'AC', so it can never match the bare one.
|
|
776
|
+
_AC_TAG_FAM = re.compile(
|
|
777
|
+
r"(?:(?<![A-Za-z0-9])[Aa][Cc]|(?<=[a-z])AC)"
|
|
778
|
+
r"[-_]([A-Za-z][A-Za-z0-9]*)[-_]0*(\d+)(?!\d)")
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def _ac_canon(family, num):
|
|
782
|
+
"""Canonical criterion id for both grammars (ADR-036): 'AC-<int>' for the bare
|
|
783
|
+
form (AC-01 == AC_1 == ac1) and 'AC-<FAMILY>-<int>' for the family form
|
|
784
|
+
(AC-T-01 == AC-T-1 == ac_t_1) — python/go test names cannot carry '-', so the
|
|
785
|
+
separator and the zero padding are never part of the identity."""
|
|
786
|
+
if family:
|
|
787
|
+
return "AC-%s-%d" % (family.upper(), int(num))
|
|
788
|
+
return "AC-%d" % int(num)
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
def _ac_tag_ids(name):
|
|
792
|
+
"""Every criterion id a testcase NAME tags, normalized. Family form first, then
|
|
793
|
+
bare; the two patterns are disjoint, so the order fixes only the output order."""
|
|
794
|
+
ids = [_ac_canon(fam, num) for fam, num in _AC_TAG_FAM.findall(name)]
|
|
795
|
+
ids.extend(_ac_canon(None, num) for num in _AC_TAG.findall(name))
|
|
796
|
+
return ids
|
|
797
|
+
|
|
767
798
|
|
|
768
799
|
def _ac_tags(repo_path, repo_type):
|
|
769
800
|
"""Tags AC-n leidos de los NOMBRES de testcase en los reportes JUnit que el
|
|
770
801
|
engine ya ingiere. Devuelve (tags, stale) donde tags = {'AC-n': {'green': x,
|
|
771
|
-
'red': y}}
|
|
802
|
+
'red': y}} (o 'AC-FAM-n' para la forma con familia, ADR-036 / kit 1.87.0)
|
|
803
|
+
y stale = [rutas de reportes descartados por viejos]. Un criterio
|
|
772
804
|
cierra MEDIDO solo con >=1 testcase verde y 0 rojos (evidencia roja veta:
|
|
773
805
|
fail-closed). Testcases skipped no cuentan para ningun lado.
|
|
774
806
|
|
|
@@ -812,9 +844,8 @@ def _ac_tags(repo_path, repo_type):
|
|
|
812
844
|
# cuyo nombre matchea 'ACn' por coincidencia (test_ac3_flow.py) no
|
|
813
845
|
# debe taggear los OTROS tests del mismo archivo/clase.
|
|
814
846
|
blob = tc.get("name") or ""
|
|
815
|
-
for
|
|
816
|
-
d = tags.setdefault(
|
|
817
|
-
{"green": 0, "red": 0, "cases": []})
|
|
847
|
+
for cid in _ac_tag_ids(blob):
|
|
848
|
+
d = tags.setdefault(cid, {"green": 0, "red": 0, "cases": []})
|
|
818
849
|
d[status] += 1
|
|
819
850
|
# RECEIPT (kit 1.50.0): keep WHICH testcase in WHICH report backed the
|
|
820
851
|
# verdict -- the name and path were always in scope here and were being
|
|
@@ -4213,15 +4244,20 @@ def _canonical_ids(repo_path, acceptance_file):
|
|
|
4213
4244
|
except Exception:
|
|
4214
4245
|
return {}
|
|
4215
4246
|
for it in items or []:
|
|
4216
|
-
if it.get("id"):
|
|
4217
|
-
|
|
4247
|
+
if it.get("id"):
|
|
4248
|
+
# normalized id of EITHER grammar (ADR-036): "AC-<n>" or "AC-<FAMILY>-<n>". Keyed by
|
|
4249
|
+
# the canonical id itself; a bare and a family id can never collide.
|
|
4250
|
+
ids[it["id"]] = it["id"]
|
|
4218
4251
|
return ids
|
|
4219
4252
|
|
|
4220
4253
|
|
|
4221
4254
|
def _match_canonical(statement, canon_ids):
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4255
|
+
"""The canonical id an observation's statement names, if any -- read with the same
|
|
4256
|
+
grammar as ACCEPTANCE.md and the JUnit tags (ADR-036), so a statement that mentions
|
|
4257
|
+
'AC-DD-07' anchors the criterion AC-DD-7 exactly as one that mentions 'AC-7' anchors AC-7."""
|
|
4258
|
+
for cid in _ac_tag_ids(statement or ""):
|
|
4259
|
+
if cid in canon_ids:
|
|
4260
|
+
return canon_ids[cid]
|
|
4225
4261
|
return None
|
|
4226
4262
|
|
|
4227
4263
|
|
|
@@ -7175,12 +7211,34 @@ def _band(score):
|
|
|
7175
7211
|
|
|
7176
7212
|
_AC_ID = re.compile(r"(?i)^[*_`]*\s*AC[-_]?0*(\d+)\b[*_`]*[\s.:—–·-]*")
|
|
7177
7213
|
|
|
7214
|
+
# kit 1.87.0 (ADR-036): the family grammar, same tolerated wrappers/separators.
|
|
7215
|
+
# A family starts with a LETTER, so a numeric "family" is not one: '- [ ] AC-7-x'
|
|
7216
|
+
# falls through to the bare pattern and reads as AC-7 followed by the text 'x' (the
|
|
7217
|
+
# trailing-separator class eats the hyphen, exactly as before ADR-036).
|
|
7218
|
+
_AC_ID_FAM = re.compile(
|
|
7219
|
+
r"(?i)^[*_`]*\s*AC[-_]([A-Za-z][A-Za-z0-9]*)[-_]0*(\d+)\b[*_`]*[\s.:—–·-]*")
|
|
7220
|
+
|
|
7221
|
+
|
|
7222
|
+
def _ac_id_of(body):
|
|
7223
|
+
"""(canonical id, end offset) of the leading AC id of a checkbox body, or
|
|
7224
|
+
(None, 0). The FAMILY form is tried first; the bare form is the fallback, so
|
|
7225
|
+
every id the engine read before ADR-036 still reads exactly the same."""
|
|
7226
|
+
m = _AC_ID_FAM.match(body)
|
|
7227
|
+
if m:
|
|
7228
|
+
return _ac_canon(m.group(1), m.group(2)), m.end()
|
|
7229
|
+
m = _AC_ID.match(body)
|
|
7230
|
+
if m:
|
|
7231
|
+
return _ac_canon(None, m.group(1)), m.end()
|
|
7232
|
+
return None, 0
|
|
7233
|
+
|
|
7178
7234
|
|
|
7179
7235
|
def _parse_acceptance_items(path, section=None):
|
|
7180
7236
|
"""Checkboxes markdown de ACCEPTANCE, con ID trazable opcional por criterio
|
|
7181
7237
|
('- [ ] AC-01 — cuando X entonces Y'). Los IDs se normalizan por numero
|
|
7182
|
-
(AC-01 == AC_1 == ac1 — los nombres de test de python/go no admiten '-')
|
|
7183
|
-
|
|
7238
|
+
(AC-01 == AC_1 == ac1 — los nombres de test de python/go no admiten '-') y,
|
|
7239
|
+
desde kit 1.87.0 (ADR-036), tambien por FAMILIA ('- [ ] AC-BC-07 — ...',
|
|
7240
|
+
AC-T-01 == AC-T-1 == ac_t_1). Devuelve (items, found);
|
|
7241
|
+
item = {'id': 'AC-n'|'AC-FAM-n'|None, 'checked', 'text'}."""
|
|
7184
7242
|
if not path or not os.path.exists(path):
|
|
7185
7243
|
return [], False
|
|
7186
7244
|
items = []
|
|
@@ -7203,10 +7261,10 @@ def _parse_acceptance_items(path, section=None):
|
|
|
7203
7261
|
else:
|
|
7204
7262
|
continue
|
|
7205
7263
|
body = s[5:].strip()
|
|
7206
|
-
|
|
7207
|
-
items.append({"id":
|
|
7264
|
+
cid, end = _ac_id_of(body)
|
|
7265
|
+
items.append({"id": cid,
|
|
7208
7266
|
"checked": checked,
|
|
7209
|
-
"text": body[
|
|
7267
|
+
"text": body[end:].strip()})
|
|
7210
7268
|
except OSError:
|
|
7211
7269
|
return [], False
|
|
7212
7270
|
return items, True
|
|
@@ -8157,12 +8215,21 @@ def _top_pct(done, total):
|
|
|
8157
8215
|
return 99 if (done < total and pct >= 100) else pct
|
|
8158
8216
|
|
|
8159
8217
|
|
|
8160
|
-
def
|
|
8161
|
-
"""Stable
|
|
8218
|
+
def _top_ac_key(cid):
|
|
8219
|
+
"""Stable order for the normalized ids _parse_acceptance_items emits: the BARE
|
|
8220
|
+
'AC-<n>' criteria first by number, then each letter FAMILY alphabetically and by
|
|
8221
|
+
number inside it (ADR-036). An id in neither shape sorts last instead of raising —
|
|
8222
|
+
the board must still render when the acceptance file carries something unexpected."""
|
|
8223
|
+
parts = str(cid).split("-")
|
|
8224
|
+
if len(parts) >= 3:
|
|
8225
|
+
try:
|
|
8226
|
+
return (1, parts[1].upper(), int(parts[2]))
|
|
8227
|
+
except ValueError:
|
|
8228
|
+
return (2, str(cid), 0)
|
|
8162
8229
|
try:
|
|
8163
|
-
return
|
|
8230
|
+
return (0, "", int(parts[1]))
|
|
8164
8231
|
except (IndexError, ValueError):
|
|
8165
|
-
return 0
|
|
8232
|
+
return (2, str(cid), 0)
|
|
8166
8233
|
|
|
8167
8234
|
|
|
8168
8235
|
def cmd_top(args):
|
|
@@ -8234,7 +8301,7 @@ def cmd_top(args):
|
|
|
8234
8301
|
seen.add(it["id"])
|
|
8235
8302
|
ids.append(it["id"])
|
|
8236
8303
|
obligations = []
|
|
8237
|
-
for cid in sorted(ids, key=
|
|
8304
|
+
for cid in sorted(ids, key=_top_ac_key):
|
|
8238
8305
|
tag, obs = ac_tags.get(cid), quarantine.get(cid)
|
|
8239
8306
|
# red evidence VETOES (fail-closed, the same rule _ac_closed applies). Measured
|
|
8240
8307
|
# evidence outranks the lateral QUARANTINE rung so the four buckets partition the
|
|
@@ -8346,13 +8413,13 @@ def cmd_readiness(args):
|
|
|
8346
8413
|
# IDs duplicados (ACCEPTANCE mal numerado) cuentan UNA sola vez — si no,
|
|
8347
8414
|
# un solo test verde cierra "medido" tantos criterios como copias del ID.
|
|
8348
8415
|
id_list = [i["id"] for i in ac_ids]
|
|
8349
|
-
dupe_ids = sorted({cid for cid in id_list if id_list.count(cid) > 1})
|
|
8350
|
-
unique_ids = sorted(set(id_list))
|
|
8416
|
+
dupe_ids = sorted({cid for cid in id_list if id_list.count(cid) > 1}, key=_top_ac_key)
|
|
8417
|
+
unique_ids = sorted(set(id_list), key=_top_ac_key)
|
|
8351
8418
|
measured_closed = [cid for cid in unique_ids if _ac_closed(cid)]
|
|
8352
8419
|
narrated_only = sorted({i["id"] for i in ac_ids
|
|
8353
|
-
if i["checked"] and not _ac_closed(i["id"])})
|
|
8420
|
+
if i["checked"] and not _ac_closed(i["id"])}, key=_top_ac_key)
|
|
8354
8421
|
measured_unchecked = sorted({i["id"] for i in ac_ids
|
|
8355
|
-
if not i["checked"] and _ac_closed(i["id"])})
|
|
8422
|
+
if not i["checked"] and _ac_closed(i["id"])}, key=_top_ac_key)
|
|
8356
8423
|
ac_untagged = total - len(ac_ids)
|
|
8357
8424
|
acc_traceable = bool(ac_ids)
|
|
8358
8425
|
if acc_traceable:
|
|
@@ -8627,8 +8694,9 @@ def cmd_readiness(args):
|
|
|
8627
8694
|
f"(--section {args.section!r} matched nothing in the file?) — "
|
|
8628
8695
|
f"adr/acceptance dimensions at 0")
|
|
8629
8696
|
if acc_found and total and not acc_traceable:
|
|
8630
|
-
print(" ! acceptance has no traceable IDs ('- [ ] AC-01 — ...'
|
|
8631
|
-
"acceptance dimension falls back to the
|
|
8697
|
+
print(" ! acceptance has no traceable IDs ('- [ ] AC-01 — ...' or "
|
|
8698
|
+
"'- [ ] AC-BC-01 — ...') — the acceptance dimension falls back to the "
|
|
8699
|
+
"checkbox ratio (NARRATED, not measured)")
|
|
8632
8700
|
if dupe_ids:
|
|
8633
8701
|
print(f" ! duplicate IDs in acceptance (normalized): {', '.join(dupe_ids)} "
|
|
8634
8702
|
f"— each ID counts ONCE in the acceptance dimension")
|
|
@@ -10102,8 +10170,9 @@ def _spec_check_text(text):
|
|
|
10102
10170
|
def _acceptance_traceability(path):
|
|
10103
10171
|
"""Trazabilidad del ACCEPTANCE (kit 1.10.0): estructura = FACT.
|
|
10104
10172
|
Bloquea: archivo ausente, cero criterios, CERO criterios con AC-ID, IDs
|
|
10105
|
-
duplicados (tras normalizar: AC-01 == AC-1
|
|
10106
|
-
sin ID (no podran cerrar
|
|
10173
|
+
duplicados (tras normalizar: AC-01 == AC-1, y desde kit 1.87.0 / ADR-036
|
|
10174
|
+
AC-BC-07 == AC-BC-7). Aconseja: criterios sueltos sin ID (no podran cerrar
|
|
10175
|
+
MEDIDO)."""
|
|
10107
10176
|
blockers, advisory = [], []
|
|
10108
10177
|
items, found = _parse_acceptance_items(path)
|
|
10109
10178
|
if not found:
|
|
@@ -10115,7 +10184,8 @@ def _acceptance_traceability(path):
|
|
|
10115
10184
|
ids = [i["id"] for i in items if i["id"]]
|
|
10116
10185
|
if not ids:
|
|
10117
10186
|
blockers.append("cero criterios trazables — cada criterio lleva ID "
|
|
10118
|
-
"estable: '- [ ] AC-01 — cuando X entonces Y'"
|
|
10187
|
+
"estable: '- [ ] AC-01 — cuando X entonces Y' "
|
|
10188
|
+
"(o con familia: '- [ ] AC-BC-01 — ...')")
|
|
10119
10189
|
return blockers, advisory
|
|
10120
10190
|
dupes = sorted({x for x in ids if ids.count(x) > 1})
|
|
10121
10191
|
if dupes:
|
|
@@ -10835,7 +10905,8 @@ def cmd_doctor(args):
|
|
|
10835
10905
|
elif items:
|
|
10836
10906
|
warn(f"ACCEPTANCE without traceable AC-IDs ({len(items)} criterion(s))",
|
|
10837
10907
|
"generate it with /uscha-discovery or /uscha-adr-refine "
|
|
10838
|
-
"(format '- [ ] AC-01 - ...'
|
|
10908
|
+
"(format '- [ ] AC-01 - ...', or with a family "
|
|
10909
|
+
"'- [ ] AC-BC-01 - ...') - without IDs the dominant readiness "
|
|
10839
10910
|
"dimension falls back to the checkbox ratio")
|
|
10840
10911
|
else:
|
|
10841
10912
|
warn(f"ACCEPTANCE {acc} has no criteria (zero checkboxes)")
|
|
@@ -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.
|
|
4
|
+
"version": "1.87.0",
|
|
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.
|
|
3
|
+
**Kit version:** v1.87.0 <!-- uscha:version --> · **[uscha.dev](https://uscha.dev)**
|
|
4
4
|
|
|
5
5
|
Spec-driven orchestrator + multi-repo QA for Claude Code, with a deterministic ledger.
|
|
6
6
|
**Nine skills** (`uscha-discovery`, `uscha-adr-refine`, `uscha-devloop`, `uscha-sysdoc`, `uscha-reverse-discovery`,
|
package/uscha-kit/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
uscha-kit 1.
|
|
1
|
+
uscha-kit 1.87.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"AC-FA-01": true, "AC-FA-02": true, "AC-FA-03": null, "AC-FA-04": true, "AC-FA-05": true, "AC-FA-06": true}
|
|
@@ -535,7 +535,10 @@ once logged it caps readiness ≤65 and blocks convergence until resolved with
|
|
|
535
535
|
criterion carries a stable ID: `- [ ] AC-01 — when X then Y`. A criterion counts as
|
|
536
536
|
CLOSED only when ≥1 GREEN testcase whose name carries the tag (`test_ac1_x`,
|
|
537
537
|
`testAC01X`, `"AC-01: ..."` — IDs normalize by number, `AC-01 == AC_1 == ac1`) exists
|
|
538
|
-
in the ingested JUnit reports AND no tagged testcase is red.
|
|
538
|
+
in the ingested JUnit reports AND no tagged testcase is red. Since kit 1.87.0 (ADR-036)
|
|
539
|
+
a FAMILY prefix is read the same way: `- [ ] AC-BC-07 — ...` closes on `AC-BC-07_x`,
|
|
540
|
+
`test_ac_bc_7_y` or `AC_BC_7` (normalized to `AC-BC-7`; the family needs a separator on
|
|
541
|
+
both sides — camelCase `testACBC07` is NOT a tag, and `AC-7-x` is still the bare `AC-7`). The checkbox is the
|
|
539
542
|
NARRATIVE; the testcase is the FACT — a checked box without a green tagged test shows
|
|
540
543
|
up as `narrated_only` and does NOT close (measured beats narrated, per criterion).
|
|
541
544
|
A JUnit report older than the repo's source code is treated as STALE (the code changed
|
|
@@ -764,11 +764,43 @@ def _test_evidence_provenance(repo_path, repo_type):
|
|
|
764
764
|
_AC_TAG = re.compile(
|
|
765
765
|
r"(?:(?<![A-Za-z0-9])[Aa][Cc]|(?<=[a-z])AC)[-_]?0*(\d+)(?!\d)")
|
|
766
766
|
|
|
767
|
+
# kit 1.87.0 (ADR-036): the FAMILY grammar 'AC-<FAMILY>-<n>' (AC-BC-07, AC-T-24,
|
|
768
|
+
# ac_dd_3). Same explicit boundaries as the bare form, plus a MANDATORY separator
|
|
769
|
+
# on BOTH sides of the family — 'AC-BC-07', 'ac_bc_7', 'AC_T_1'. camelCase families
|
|
770
|
+
# are deliberately NOT supported: 'testACBC07' has no honest split into family +
|
|
771
|
+
# number and stays unmatched.
|
|
772
|
+
# Kept as its OWN pattern so the bare pattern above (and every tag it already
|
|
773
|
+
# produced) stays byte-identical. The two are disjoint by construction: a family
|
|
774
|
+
# must start with a LETTER, so 'AC-01' can never match this one, and the digits of
|
|
775
|
+
# 'AC-BC-07' do not follow 'AC', so it can never match the bare one.
|
|
776
|
+
_AC_TAG_FAM = re.compile(
|
|
777
|
+
r"(?:(?<![A-Za-z0-9])[Aa][Cc]|(?<=[a-z])AC)"
|
|
778
|
+
r"[-_]([A-Za-z][A-Za-z0-9]*)[-_]0*(\d+)(?!\d)")
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def _ac_canon(family, num):
|
|
782
|
+
"""Canonical criterion id for both grammars (ADR-036): 'AC-<int>' for the bare
|
|
783
|
+
form (AC-01 == AC_1 == ac1) and 'AC-<FAMILY>-<int>' for the family form
|
|
784
|
+
(AC-T-01 == AC-T-1 == ac_t_1) — python/go test names cannot carry '-', so the
|
|
785
|
+
separator and the zero padding are never part of the identity."""
|
|
786
|
+
if family:
|
|
787
|
+
return "AC-%s-%d" % (family.upper(), int(num))
|
|
788
|
+
return "AC-%d" % int(num)
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
def _ac_tag_ids(name):
|
|
792
|
+
"""Every criterion id a testcase NAME tags, normalized. Family form first, then
|
|
793
|
+
bare; the two patterns are disjoint, so the order fixes only the output order."""
|
|
794
|
+
ids = [_ac_canon(fam, num) for fam, num in _AC_TAG_FAM.findall(name)]
|
|
795
|
+
ids.extend(_ac_canon(None, num) for num in _AC_TAG.findall(name))
|
|
796
|
+
return ids
|
|
797
|
+
|
|
767
798
|
|
|
768
799
|
def _ac_tags(repo_path, repo_type):
|
|
769
800
|
"""Tags AC-n leidos de los NOMBRES de testcase en los reportes JUnit que el
|
|
770
801
|
engine ya ingiere. Devuelve (tags, stale) donde tags = {'AC-n': {'green': x,
|
|
771
|
-
'red': y}}
|
|
802
|
+
'red': y}} (o 'AC-FAM-n' para la forma con familia, ADR-036 / kit 1.87.0)
|
|
803
|
+
y stale = [rutas de reportes descartados por viejos]. Un criterio
|
|
772
804
|
cierra MEDIDO solo con >=1 testcase verde y 0 rojos (evidencia roja veta:
|
|
773
805
|
fail-closed). Testcases skipped no cuentan para ningun lado.
|
|
774
806
|
|
|
@@ -812,9 +844,8 @@ def _ac_tags(repo_path, repo_type):
|
|
|
812
844
|
# cuyo nombre matchea 'ACn' por coincidencia (test_ac3_flow.py) no
|
|
813
845
|
# debe taggear los OTROS tests del mismo archivo/clase.
|
|
814
846
|
blob = tc.get("name") or ""
|
|
815
|
-
for
|
|
816
|
-
d = tags.setdefault(
|
|
817
|
-
{"green": 0, "red": 0, "cases": []})
|
|
847
|
+
for cid in _ac_tag_ids(blob):
|
|
848
|
+
d = tags.setdefault(cid, {"green": 0, "red": 0, "cases": []})
|
|
818
849
|
d[status] += 1
|
|
819
850
|
# RECEIPT (kit 1.50.0): keep WHICH testcase in WHICH report backed the
|
|
820
851
|
# verdict -- the name and path were always in scope here and were being
|
|
@@ -4213,15 +4244,20 @@ def _canonical_ids(repo_path, acceptance_file):
|
|
|
4213
4244
|
except Exception:
|
|
4214
4245
|
return {}
|
|
4215
4246
|
for it in items or []:
|
|
4216
|
-
if it.get("id"):
|
|
4217
|
-
|
|
4247
|
+
if it.get("id"):
|
|
4248
|
+
# normalized id of EITHER grammar (ADR-036): "AC-<n>" or "AC-<FAMILY>-<n>". Keyed by
|
|
4249
|
+
# the canonical id itself; a bare and a family id can never collide.
|
|
4250
|
+
ids[it["id"]] = it["id"]
|
|
4218
4251
|
return ids
|
|
4219
4252
|
|
|
4220
4253
|
|
|
4221
4254
|
def _match_canonical(statement, canon_ids):
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4255
|
+
"""The canonical id an observation's statement names, if any -- read with the same
|
|
4256
|
+
grammar as ACCEPTANCE.md and the JUnit tags (ADR-036), so a statement that mentions
|
|
4257
|
+
'AC-DD-07' anchors the criterion AC-DD-7 exactly as one that mentions 'AC-7' anchors AC-7."""
|
|
4258
|
+
for cid in _ac_tag_ids(statement or ""):
|
|
4259
|
+
if cid in canon_ids:
|
|
4260
|
+
return canon_ids[cid]
|
|
4225
4261
|
return None
|
|
4226
4262
|
|
|
4227
4263
|
|
|
@@ -7175,12 +7211,34 @@ def _band(score):
|
|
|
7175
7211
|
|
|
7176
7212
|
_AC_ID = re.compile(r"(?i)^[*_`]*\s*AC[-_]?0*(\d+)\b[*_`]*[\s.:—–·-]*")
|
|
7177
7213
|
|
|
7214
|
+
# kit 1.87.0 (ADR-036): the family grammar, same tolerated wrappers/separators.
|
|
7215
|
+
# A family starts with a LETTER, so a numeric "family" is not one: '- [ ] AC-7-x'
|
|
7216
|
+
# falls through to the bare pattern and reads as AC-7 followed by the text 'x' (the
|
|
7217
|
+
# trailing-separator class eats the hyphen, exactly as before ADR-036).
|
|
7218
|
+
_AC_ID_FAM = re.compile(
|
|
7219
|
+
r"(?i)^[*_`]*\s*AC[-_]([A-Za-z][A-Za-z0-9]*)[-_]0*(\d+)\b[*_`]*[\s.:—–·-]*")
|
|
7220
|
+
|
|
7221
|
+
|
|
7222
|
+
def _ac_id_of(body):
|
|
7223
|
+
"""(canonical id, end offset) of the leading AC id of a checkbox body, or
|
|
7224
|
+
(None, 0). The FAMILY form is tried first; the bare form is the fallback, so
|
|
7225
|
+
every id the engine read before ADR-036 still reads exactly the same."""
|
|
7226
|
+
m = _AC_ID_FAM.match(body)
|
|
7227
|
+
if m:
|
|
7228
|
+
return _ac_canon(m.group(1), m.group(2)), m.end()
|
|
7229
|
+
m = _AC_ID.match(body)
|
|
7230
|
+
if m:
|
|
7231
|
+
return _ac_canon(None, m.group(1)), m.end()
|
|
7232
|
+
return None, 0
|
|
7233
|
+
|
|
7178
7234
|
|
|
7179
7235
|
def _parse_acceptance_items(path, section=None):
|
|
7180
7236
|
"""Checkboxes markdown de ACCEPTANCE, con ID trazable opcional por criterio
|
|
7181
7237
|
('- [ ] AC-01 — cuando X entonces Y'). Los IDs se normalizan por numero
|
|
7182
|
-
(AC-01 == AC_1 == ac1 — los nombres de test de python/go no admiten '-')
|
|
7183
|
-
|
|
7238
|
+
(AC-01 == AC_1 == ac1 — los nombres de test de python/go no admiten '-') y,
|
|
7239
|
+
desde kit 1.87.0 (ADR-036), tambien por FAMILIA ('- [ ] AC-BC-07 — ...',
|
|
7240
|
+
AC-T-01 == AC-T-1 == ac_t_1). Devuelve (items, found);
|
|
7241
|
+
item = {'id': 'AC-n'|'AC-FAM-n'|None, 'checked', 'text'}."""
|
|
7184
7242
|
if not path or not os.path.exists(path):
|
|
7185
7243
|
return [], False
|
|
7186
7244
|
items = []
|
|
@@ -7203,10 +7261,10 @@ def _parse_acceptance_items(path, section=None):
|
|
|
7203
7261
|
else:
|
|
7204
7262
|
continue
|
|
7205
7263
|
body = s[5:].strip()
|
|
7206
|
-
|
|
7207
|
-
items.append({"id":
|
|
7264
|
+
cid, end = _ac_id_of(body)
|
|
7265
|
+
items.append({"id": cid,
|
|
7208
7266
|
"checked": checked,
|
|
7209
|
-
"text": body[
|
|
7267
|
+
"text": body[end:].strip()})
|
|
7210
7268
|
except OSError:
|
|
7211
7269
|
return [], False
|
|
7212
7270
|
return items, True
|
|
@@ -8157,12 +8215,21 @@ def _top_pct(done, total):
|
|
|
8157
8215
|
return 99 if (done < total and pct >= 100) else pct
|
|
8158
8216
|
|
|
8159
8217
|
|
|
8160
|
-
def
|
|
8161
|
-
"""Stable
|
|
8218
|
+
def _top_ac_key(cid):
|
|
8219
|
+
"""Stable order for the normalized ids _parse_acceptance_items emits: the BARE
|
|
8220
|
+
'AC-<n>' criteria first by number, then each letter FAMILY alphabetically and by
|
|
8221
|
+
number inside it (ADR-036). An id in neither shape sorts last instead of raising —
|
|
8222
|
+
the board must still render when the acceptance file carries something unexpected."""
|
|
8223
|
+
parts = str(cid).split("-")
|
|
8224
|
+
if len(parts) >= 3:
|
|
8225
|
+
try:
|
|
8226
|
+
return (1, parts[1].upper(), int(parts[2]))
|
|
8227
|
+
except ValueError:
|
|
8228
|
+
return (2, str(cid), 0)
|
|
8162
8229
|
try:
|
|
8163
|
-
return
|
|
8230
|
+
return (0, "", int(parts[1]))
|
|
8164
8231
|
except (IndexError, ValueError):
|
|
8165
|
-
return 0
|
|
8232
|
+
return (2, str(cid), 0)
|
|
8166
8233
|
|
|
8167
8234
|
|
|
8168
8235
|
def cmd_top(args):
|
|
@@ -8234,7 +8301,7 @@ def cmd_top(args):
|
|
|
8234
8301
|
seen.add(it["id"])
|
|
8235
8302
|
ids.append(it["id"])
|
|
8236
8303
|
obligations = []
|
|
8237
|
-
for cid in sorted(ids, key=
|
|
8304
|
+
for cid in sorted(ids, key=_top_ac_key):
|
|
8238
8305
|
tag, obs = ac_tags.get(cid), quarantine.get(cid)
|
|
8239
8306
|
# red evidence VETOES (fail-closed, the same rule _ac_closed applies). Measured
|
|
8240
8307
|
# evidence outranks the lateral QUARANTINE rung so the four buckets partition the
|
|
@@ -8346,13 +8413,13 @@ def cmd_readiness(args):
|
|
|
8346
8413
|
# IDs duplicados (ACCEPTANCE mal numerado) cuentan UNA sola vez — si no,
|
|
8347
8414
|
# un solo test verde cierra "medido" tantos criterios como copias del ID.
|
|
8348
8415
|
id_list = [i["id"] for i in ac_ids]
|
|
8349
|
-
dupe_ids = sorted({cid for cid in id_list if id_list.count(cid) > 1})
|
|
8350
|
-
unique_ids = sorted(set(id_list))
|
|
8416
|
+
dupe_ids = sorted({cid for cid in id_list if id_list.count(cid) > 1}, key=_top_ac_key)
|
|
8417
|
+
unique_ids = sorted(set(id_list), key=_top_ac_key)
|
|
8351
8418
|
measured_closed = [cid for cid in unique_ids if _ac_closed(cid)]
|
|
8352
8419
|
narrated_only = sorted({i["id"] for i in ac_ids
|
|
8353
|
-
if i["checked"] and not _ac_closed(i["id"])})
|
|
8420
|
+
if i["checked"] and not _ac_closed(i["id"])}, key=_top_ac_key)
|
|
8354
8421
|
measured_unchecked = sorted({i["id"] for i in ac_ids
|
|
8355
|
-
if not i["checked"] and _ac_closed(i["id"])})
|
|
8422
|
+
if not i["checked"] and _ac_closed(i["id"])}, key=_top_ac_key)
|
|
8356
8423
|
ac_untagged = total - len(ac_ids)
|
|
8357
8424
|
acc_traceable = bool(ac_ids)
|
|
8358
8425
|
if acc_traceable:
|
|
@@ -8627,8 +8694,9 @@ def cmd_readiness(args):
|
|
|
8627
8694
|
f"(--section {args.section!r} matched nothing in the file?) — "
|
|
8628
8695
|
f"adr/acceptance dimensions at 0")
|
|
8629
8696
|
if acc_found and total and not acc_traceable:
|
|
8630
|
-
print(" ! acceptance has no traceable IDs ('- [ ] AC-01 — ...'
|
|
8631
|
-
"acceptance dimension falls back to the
|
|
8697
|
+
print(" ! acceptance has no traceable IDs ('- [ ] AC-01 — ...' or "
|
|
8698
|
+
"'- [ ] AC-BC-01 — ...') — the acceptance dimension falls back to the "
|
|
8699
|
+
"checkbox ratio (NARRATED, not measured)")
|
|
8632
8700
|
if dupe_ids:
|
|
8633
8701
|
print(f" ! duplicate IDs in acceptance (normalized): {', '.join(dupe_ids)} "
|
|
8634
8702
|
f"— each ID counts ONCE in the acceptance dimension")
|
|
@@ -10102,8 +10170,9 @@ def _spec_check_text(text):
|
|
|
10102
10170
|
def _acceptance_traceability(path):
|
|
10103
10171
|
"""Trazabilidad del ACCEPTANCE (kit 1.10.0): estructura = FACT.
|
|
10104
10172
|
Bloquea: archivo ausente, cero criterios, CERO criterios con AC-ID, IDs
|
|
10105
|
-
duplicados (tras normalizar: AC-01 == AC-1
|
|
10106
|
-
sin ID (no podran cerrar
|
|
10173
|
+
duplicados (tras normalizar: AC-01 == AC-1, y desde kit 1.87.0 / ADR-036
|
|
10174
|
+
AC-BC-07 == AC-BC-7). Aconseja: criterios sueltos sin ID (no podran cerrar
|
|
10175
|
+
MEDIDO)."""
|
|
10107
10176
|
blockers, advisory = [], []
|
|
10108
10177
|
items, found = _parse_acceptance_items(path)
|
|
10109
10178
|
if not found:
|
|
@@ -10115,7 +10184,8 @@ def _acceptance_traceability(path):
|
|
|
10115
10184
|
ids = [i["id"] for i in items if i["id"]]
|
|
10116
10185
|
if not ids:
|
|
10117
10186
|
blockers.append("cero criterios trazables — cada criterio lleva ID "
|
|
10118
|
-
"estable: '- [ ] AC-01 — cuando X entonces Y'"
|
|
10187
|
+
"estable: '- [ ] AC-01 — cuando X entonces Y' "
|
|
10188
|
+
"(o con familia: '- [ ] AC-BC-01 — ...')")
|
|
10119
10189
|
return blockers, advisory
|
|
10120
10190
|
dupes = sorted({x for x in ids if ids.count(x) > 1})
|
|
10121
10191
|
if dupes:
|
|
@@ -10835,7 +10905,8 @@ def cmd_doctor(args):
|
|
|
10835
10905
|
elif items:
|
|
10836
10906
|
warn(f"ACCEPTANCE without traceable AC-IDs ({len(items)} criterion(s))",
|
|
10837
10907
|
"generate it with /uscha-discovery or /uscha-adr-refine "
|
|
10838
|
-
"(format '- [ ] AC-01 - ...'
|
|
10908
|
+
"(format '- [ ] AC-01 - ...', or with a family "
|
|
10909
|
+
"'- [ ] AC-BC-01 - ...') - without IDs the dominant readiness "
|
|
10839
10910
|
"dimension falls back to the checkbox ratio")
|
|
10840
10911
|
else:
|
|
10841
10912
|
warn(f"ACCEPTANCE {acc} has no criteria (zero checkboxes)")
|
|
@@ -20,7 +20,12 @@ CONFIG = ROOT / "uscha.config.json"
|
|
|
20
20
|
OUT = ROOT / ".claude" / "uscha-progress.json"
|
|
21
21
|
|
|
22
22
|
# tolerant of bold/plain AC ids -- mirrors qa_ledger.py:_AC_ID (AC-01, **AC-01**, `AC_1`)
|
|
23
|
-
|
|
23
|
+
# and, since kit 1.87.0 (ADR-036), the FAMILY form too (AC-BC-07, AC-T-24, `ac_dd_3`).
|
|
24
|
+
# A family starts with a LETTER, so 'AC-7-x' stays the bare AC-7 followed by text --
|
|
25
|
+
# the same fallback order the engine applies. The statusline must never count a
|
|
26
|
+
# criterion the ledger cannot see, nor miss one it can.
|
|
27
|
+
_AC_CORE = r"AC(?:[-_][A-Za-z][A-Za-z0-9]*[-_]|[-_]?)\d+"
|
|
28
|
+
_AC = r"[*_`]*\s*" + _AC_CORE + r"[*_`]*"
|
|
24
29
|
|
|
25
30
|
|
|
26
31
|
def _statusline_repo(cfg):
|
|
@@ -91,7 +96,8 @@ def _acceptance(state, cfg):
|
|
|
91
96
|
# a narrated number with the same face as a measured one is the exact dishonesty
|
|
92
97
|
# this kit exists to remove (kit 1.48.1).
|
|
93
98
|
state["acceptance_source"] = "narrated"
|
|
94
|
-
m = re.search(r"- \[ \]\s*[*_`]*\s*(
|
|
99
|
+
m = re.search(r"- \[ \]\s*[*_`]*\s*(" + _AC_CORE + r")[*_`]*\s*[—–-]\s*(.+)",
|
|
100
|
+
text, re.IGNORECASE)
|
|
95
101
|
if m:
|
|
96
102
|
nxt = re.sub(r"[*_`]", "", m.group(2)).strip()
|
|
97
103
|
state["next"] = f"{m.group(1)}: {nxt[:44]}"
|