@andresmassello/uscha 1.87.0 → 1.89.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/qa_ledger.py +221 -10
- package/uscha-kit/.claude/skills/uscha-devloop/uscha_top.py +589 -37
- 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 +8 -1
- package/uscha-kit/reports/junit/.top-cases.json +1 -1
- package/uscha-kit/skills/uscha-devloop/qa_ledger.py +221 -10
- package/uscha-kit/skills/uscha-devloop/uscha_top.py +589 -37
- 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.89.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.89.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.89.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",
|
|
@@ -8232,6 +8232,215 @@ def _top_ac_key(cid):
|
|
|
8232
8232
|
return (2, str(cid), 0)
|
|
8233
8233
|
|
|
8234
8234
|
|
|
8235
|
+
TOP_EVENTS_TAIL = 8 # how many steps the feed carries; the TUI shows what fits
|
|
8236
|
+
TOP_EVENT_WIDTH = 72 # one feed line, short enough to survive the 80-column floor
|
|
8237
|
+
TOP_OBS_TITLE_WIDTH = 72 # the verdict queue's one-line label; the CLAIM is never capped
|
|
8238
|
+
|
|
8239
|
+
# kind -> level, the FIXED map ADR-032 (amended 1.88.0, M2) requires. `level` and `text` do
|
|
8240
|
+
# not exist in `ledger["steps"]`; they are derived here, once, so the TUI renders a feed it
|
|
8241
|
+
# did not author (ADR-034). A kind absent from this map reads `info`: an unclassified step
|
|
8242
|
+
# is never a green one. Four kinds get their level REFINED below from the record the step
|
|
8243
|
+
# announces (the iteration/escalation with the same `n`, the k-th clean-room record of the
|
|
8244
|
+
# repo) -- and when that correlation misses, the level stays at its neutral value instead of
|
|
8245
|
+
# guessing a verdict.
|
|
8246
|
+
TOP_EVENT_LEVELS = {
|
|
8247
|
+
"snapshot": "info", # -> fail when the snapshot recorded red tests
|
|
8248
|
+
"qa-step": "info", # -> pass when nothing was reported, or all fixed
|
|
8249
|
+
"static-gate": "info", # -> pass/fail by the gated finding count
|
|
8250
|
+
"cleanroom": "info", # -> pass/fail by the record's `ok`
|
|
8251
|
+
"fastpath-eval": "info",
|
|
8252
|
+
"gate-not-run": "unmeasured", # a gate nobody ran is UNMEASURED, not a pass
|
|
8253
|
+
"escalation": "human",
|
|
8254
|
+
"escalation-resolved": "human",
|
|
8255
|
+
"production-finding": "human",
|
|
8256
|
+
"production-finding:resolve": "human",
|
|
8257
|
+
"spec-doubt": "human",
|
|
8258
|
+
"spec-doubt:resolve": "human",
|
|
8259
|
+
"spec-change-request": "human",
|
|
8260
|
+
"spec-change-request:resolve": "human",
|
|
8261
|
+
}
|
|
8262
|
+
|
|
8263
|
+
|
|
8264
|
+
def _top_ts(iso):
|
|
8265
|
+
"""HH:MM:SS in UTC, or None. A stamp carrying an offset is normalized to UTC (machine-
|
|
8266
|
+
independent); it is never converted to the LOCAL zone, which would make the same ledger
|
|
8267
|
+
read differently on two boxes and break the golden frames."""
|
|
8268
|
+
dt = _top_dt(iso)
|
|
8269
|
+
if dt is None:
|
|
8270
|
+
return None
|
|
8271
|
+
if dt.tzinfo is not None:
|
|
8272
|
+
dt = dt.astimezone(timezone.utc)
|
|
8273
|
+
return dt.strftime("%H:%M:%S")
|
|
8274
|
+
|
|
8275
|
+
|
|
8276
|
+
def _top_key(value):
|
|
8277
|
+
"""A dict key that cannot raise. The writers always put a scalar in `n`, `at` and `repo`,
|
|
8278
|
+
but the ledger is JSON on disk and a hand edit can leave a list or a dict there --
|
|
8279
|
+
`unhashable type` is not how a read-only readout gets to report that."""
|
|
8280
|
+
if isinstance(value, (str, int, float, bool)) or value is None:
|
|
8281
|
+
return value
|
|
8282
|
+
return repr(value)
|
|
8283
|
+
|
|
8284
|
+
|
|
8285
|
+
def _top_clean(text):
|
|
8286
|
+
"""One state-supplied string with every control character gone and its whitespace
|
|
8287
|
+
collapsed. The contract's free text is ledger prose (an escalation reason, an
|
|
8288
|
+
observation statement, a tool name) -- human and CLI input -- so an ESC or a C0 byte
|
|
8289
|
+
inside one would be a control sequence the board obeys instead of prints. It dies HERE,
|
|
8290
|
+
in the engine, and the renderer drops it again on the way out: two cheap guards over one
|
|
8291
|
+
attack surface. What is filtered is exactly C0 and DEL (ADR-032)."""
|
|
8292
|
+
txt = "".join(" " if c in ("\t", "\n", "\r") else c for c in str(text))
|
|
8293
|
+
txt = "".join(c for c in txt if ord(c) >= 32 and ord(c) != 127)
|
|
8294
|
+
return " ".join(txt.split())
|
|
8295
|
+
|
|
8296
|
+
|
|
8297
|
+
def _top_cap(text, width):
|
|
8298
|
+
"""A capped string that SAYS it was cut. Never used on a claim the human has to judge --
|
|
8299
|
+
only on labels (a feed line, an observation title), whose full text the contract carries
|
|
8300
|
+
somewhere else (M3: `candidate[]` holds the whole claim, `title` only its head)."""
|
|
8301
|
+
return text if len(text) <= width else text[:width - 1] + "…"
|
|
8302
|
+
|
|
8303
|
+
|
|
8304
|
+
def _top_event_text(*parts):
|
|
8305
|
+
"""One feed line: free text of the contract that reaches a terminal, sanitized and
|
|
8306
|
+
capped to survive the 80-column floor."""
|
|
8307
|
+
return _top_cap(_top_clean(" · ".join(str(p) for p in parts
|
|
8308
|
+
if p not in (None, "", "?"))), TOP_EVENT_WIDTH)
|
|
8309
|
+
|
|
8310
|
+
|
|
8311
|
+
def _top_obs_view(o, repo):
|
|
8312
|
+
"""ONE uncurated observation as the verdicts queue reads it (ADR-032, amended 1.89.0 for
|
|
8313
|
+
M3). The delta record is the only source: `type`, `statement`, `provenance.files`,
|
|
8314
|
+
`evidence_class` -- nothing here is inferred, and a member the delta does not carry comes
|
|
8315
|
+
out empty rather than guessed.
|
|
8316
|
+
|
|
8317
|
+
- `repo` is the delta's OWN repo, and it is in the contract because the write path needs
|
|
8318
|
+
it: `curate` takes `--repo`, so a queue without it would force the TUI to pick one --
|
|
8319
|
+
the one derivation the TUI is never allowed to make (ADR-033).
|
|
8320
|
+
- `title` is the statement's HEAD, capped and marked with an ellipsis when cut. It is a
|
|
8321
|
+
label for the list line only. The claim the human judges is in `candidate[]` in full:
|
|
8322
|
+
a verdict recorded on half a sentence is the failure this split exists to prevent.
|
|
8323
|
+
- a delta is JSON on disk, so `provenance` can arrive as a list from a hand edit -- the
|
|
8324
|
+
read-only readout degrades that observation to no evidence line, it does not raise."""
|
|
8325
|
+
prov = o.get("provenance") if isinstance(o.get("provenance"), dict) else {}
|
|
8326
|
+
raw = prov.get("files") if isinstance(prov.get("files"), list) else []
|
|
8327
|
+
files = [f for f in (_top_clean(x) for x in raw) if f]
|
|
8328
|
+
statement = _top_clean(o.get("statement"))
|
|
8329
|
+
otype = _top_clean(o.get("type"))
|
|
8330
|
+
site = files[0] if files else ""
|
|
8331
|
+
candidate = ["type: %s%s" % (otype or "?", (" · site: %s" % site) if site else "")]
|
|
8332
|
+
if statement:
|
|
8333
|
+
candidate.append("claim: " + statement) # WHOLE, never capped
|
|
8334
|
+
evidence = list(files)
|
|
8335
|
+
cls, tool = _top_clean(o.get("evidence_class")), _top_clean(prov.get("tool"))
|
|
8336
|
+
if cls or tool:
|
|
8337
|
+
evidence.append("evidence_class: %s%s" % (cls or "?",
|
|
8338
|
+
(" · tool: %s" % tool) if tool else ""))
|
|
8339
|
+
return {"id": o.get("id"), "ac": o.get("canonical_match"), "repo": repo,
|
|
8340
|
+
"title": _top_cap(statement, TOP_OBS_TITLE_WIDTH) if statement else None,
|
|
8341
|
+
"candidate": candidate, "evidence": evidence,
|
|
8342
|
+
# no per-observation first-seen timestamp exists (ADR-032/035): the queue is
|
|
8343
|
+
# ordered by the criterion it anchors, not by an age nobody recorded.
|
|
8344
|
+
"age_hours": None}
|
|
8345
|
+
|
|
8346
|
+
|
|
8347
|
+
def _top_events(ledger, limit=TOP_EVENTS_TAIL):
|
|
8348
|
+
"""events_tail[]: the last `limit` steps as {ts, level, text}, NEWEST FIRST.
|
|
8349
|
+
|
|
8350
|
+
Deterministic given the ledger and read-only, like the rest of `cmd_top`. The step
|
|
8351
|
+
records carry `n, at, kind, repo` plus a few per-kind fields; everything else the feed
|
|
8352
|
+
shows comes from the record that step announces, correlated the way the ledger really
|
|
8353
|
+
supports it: by `n` for iterations, escalations and fast-path entries (the writer copies
|
|
8354
|
+
the counter into both), in ORDER for clean-room records (step and record are appended in
|
|
8355
|
+
the same call), and by `(repo, at)` for snapshots. A miss degrades that one line to
|
|
8356
|
+
`info` -- under-claiming a verdict, never inventing one."""
|
|
8357
|
+
nodes = dict(ledger.get("repos") or {})
|
|
8358
|
+
nodes["integration"] = ledger.get("integration") or {}
|
|
8359
|
+
iters, snaps = {}, {}
|
|
8360
|
+
for rname, node in nodes.items():
|
|
8361
|
+
for it in (node or {}).get("iterations") or []:
|
|
8362
|
+
if isinstance(it, dict) and it.get("n") is not None:
|
|
8363
|
+
iters[(rname, _top_key(it.get("n")))] = it
|
|
8364
|
+
for sn in (node or {}).get("snapshots") or []:
|
|
8365
|
+
if isinstance(sn, dict):
|
|
8366
|
+
snaps.setdefault((rname, _top_key(sn.get("at"))), sn)
|
|
8367
|
+
esc = {_top_key(e.get("n")): e for e in ledger.get("escalations") or []
|
|
8368
|
+
if isinstance(e, dict) and e.get("n") is not None}
|
|
8369
|
+
fastp = {_top_key(e.get("n")): e for e in ledger.get("fast_path") or []
|
|
8370
|
+
if isinstance(e, dict) and e.get("n") is not None}
|
|
8371
|
+
crs = {}
|
|
8372
|
+
for rec in ledger.get(CLEAN_ROOM_KEY) or []:
|
|
8373
|
+
if isinstance(rec, dict):
|
|
8374
|
+
crs.setdefault(str(rec.get("repo") or ""), []).append(rec)
|
|
8375
|
+
cr_seen = {}
|
|
8376
|
+
|
|
8377
|
+
events = []
|
|
8378
|
+
for st in ledger.get("steps") or []:
|
|
8379
|
+
if not isinstance(st, dict):
|
|
8380
|
+
continue
|
|
8381
|
+
kind = str(st.get("kind") or "")
|
|
8382
|
+
level = TOP_EVENT_LEVELS.get(kind, "info")
|
|
8383
|
+
try:
|
|
8384
|
+
repo = str(st.get("repo") or "")
|
|
8385
|
+
n = _top_key(st.get("n"))
|
|
8386
|
+
head, tail = kind or "step", None
|
|
8387
|
+
|
|
8388
|
+
if kind == "snapshot":
|
|
8389
|
+
head = "snapshot " + repo if repo else "snapshot"
|
|
8390
|
+
tail = "phase %s" % st.get("phase") if st.get("phase") else None
|
|
8391
|
+
tests = (snaps.get((repo, _top_key(st.get("at")))) or {}).get("tests") or {}
|
|
8392
|
+
red = (tests.get("failures") or 0) + (tests.get("errors") or 0)
|
|
8393
|
+
if tests.get("report_found") and red:
|
|
8394
|
+
level, tail = "fail", "%d red test(s)" % red
|
|
8395
|
+
elif kind in ("qa-step", "static-gate", "gate-not-run"):
|
|
8396
|
+
head = "%s %s" % (kind, "/".join(str(p) for p in (repo, st.get("tool")) if p))
|
|
8397
|
+
it = iters.get((repo, n)) or {}
|
|
8398
|
+
rep, fixed = it.get("reported"), it.get("fixed")
|
|
8399
|
+
gated = it.get("gated_reported")
|
|
8400
|
+
if kind == "gate-not-run":
|
|
8401
|
+
tail = "not run — nobody measured it"
|
|
8402
|
+
elif kind == "static-gate" and isinstance(gated, int):
|
|
8403
|
+
level = "fail" if gated >= 1 else "pass"
|
|
8404
|
+
tail = "%d gated finding(s)" % gated if gated else "clean"
|
|
8405
|
+
elif kind == "qa-step" and isinstance(rep, int):
|
|
8406
|
+
if rep == 0 or (isinstance(fixed, int) and fixed >= rep):
|
|
8407
|
+
level = "pass"
|
|
8408
|
+
tail = "%d reported, %s fixed" % (rep,
|
|
8409
|
+
fixed if fixed is not None else "?")
|
|
8410
|
+
elif kind == "cleanroom":
|
|
8411
|
+
head = "cleanroom " + repo if repo else "cleanroom"
|
|
8412
|
+
queue = crs.get(repo) or []
|
|
8413
|
+
idx = cr_seen.get(repo, 0)
|
|
8414
|
+
cr_seen[repo] = idx + 1
|
|
8415
|
+
rec = queue[idx] if idx < len(queue) else {}
|
|
8416
|
+
if rec.get("status") and rec.get("ok") is not None:
|
|
8417
|
+
level = "pass" if rec.get("ok") else "fail"
|
|
8418
|
+
tail = str(rec.get("status"))
|
|
8419
|
+
elif kind == "fastpath-eval":
|
|
8420
|
+
head = "fastpath-eval " + repo if repo else "fastpath-eval"
|
|
8421
|
+
tail = (fastp.get(n) or {}).get("verdict")
|
|
8422
|
+
elif kind in ("escalation", "escalation-resolved"):
|
|
8423
|
+
head = "%s %s" % (kind, repo) if repo else kind
|
|
8424
|
+
# `escalation-resolved` gets a FRESH counter of its own, so there is no
|
|
8425
|
+
# record to look up: it says what happened and nothing more.
|
|
8426
|
+
tail = (esc.get(n) or {}).get("reason") if kind == "escalation" else None
|
|
8427
|
+
else:
|
|
8428
|
+
head = "%s %s" % (kind, repo) if repo else (kind or "step")
|
|
8429
|
+
tail = st.get("id")
|
|
8430
|
+
text = _top_event_text(head, tail)
|
|
8431
|
+
except Exception:
|
|
8432
|
+
# a ledger is JSON on disk: any field can arrive as a list, a dict or a number
|
|
8433
|
+
# from a hand edit. ONE unreadable step degrades to a neutral line naming its
|
|
8434
|
+
# kind -- the readout never raises and never loses the JSON the board needs
|
|
8435
|
+
# (the same fail-soft rule _top_dt already applies to timestamps).
|
|
8436
|
+
level, text = "info", _top_event_text(kind or "step")
|
|
8437
|
+
|
|
8438
|
+
events.append({"ts": _top_ts(st.get("at")), "level": level, "text": text})
|
|
8439
|
+
|
|
8440
|
+
events.reverse() # newest first, as the board reads top-down
|
|
8441
|
+
return events[:max(0, int(limit))]
|
|
8442
|
+
|
|
8443
|
+
|
|
8235
8444
|
def cmd_top(args):
|
|
8236
8445
|
"""`uscha top` — the WHOLE projection of the ledger as one read-only JSON (ADR-032).
|
|
8237
8446
|
|
|
@@ -8282,15 +8491,15 @@ def cmd_top(args):
|
|
|
8282
8491
|
cid = o.get("canonical_match")
|
|
8283
8492
|
if cid and cid not in quarantine:
|
|
8284
8493
|
quarantine[cid] = o["id"]
|
|
8285
|
-
observations.append(
|
|
8286
|
-
|
|
8287
|
-
|
|
8288
|
-
|
|
8289
|
-
|
|
8290
|
-
|
|
8291
|
-
|
|
8292
|
-
|
|
8293
|
-
|
|
8494
|
+
observations.append(_top_obs_view(o, rname))
|
|
8495
|
+
# the queue's order: the criterion each observation anchors first (by the same key the
|
|
8496
|
+
# board orders obligations with), the unanchored ones after, and the content-addressed
|
|
8497
|
+
# id as the tie-break. Deterministic given the delta -- which is what lets a golden frame
|
|
8498
|
+
# be the oracle for the verdicts pane. AGE-descending, which the SPEC drafted, is not
|
|
8499
|
+
# derivable: every age_hours is null (no first-seen timestamp), and ordering by a value
|
|
8500
|
+
# that does not exist would be the fabrication INV-TOP-05 forbids.
|
|
8501
|
+
observations.sort(key=lambda o: ((_top_ac_key(o["ac"]) if o.get("ac") else (3, "", 0)),
|
|
8502
|
+
o.get("id") or ""))
|
|
8294
8503
|
|
|
8295
8504
|
# obligations: one row per DISTINCT tagged criterion of the acceptance file. kind is
|
|
8296
8505
|
# "AC" for all of them -- there is no per-INV ledger in the general path (the mirador's
|
|
@@ -8348,7 +8557,9 @@ def cmd_top(args):
|
|
|
8348
8557
|
"generated_at": _now(),
|
|
8349
8558
|
"obligations": obligations,
|
|
8350
8559
|
"observations": observations,
|
|
8351
|
-
|
|
8560
|
+
# the live feed (M2): the last steps, newest first, with `level`/`text` derived by the
|
|
8561
|
+
# fixed per-kind map above -- in the engine, so the TUI authors no verdict of its own.
|
|
8562
|
+
"events_tail": _top_events(ledger),
|
|
8352
8563
|
"counts": {"measured_pass": done, "measured_fail": fail, "quarantine": quar,
|
|
8353
8564
|
"unmeasured": _n("UNMEASURED"), "traced": 0, "tagged": 0, "total": total},
|
|
8354
8565
|
"terminado": {"done": done, "total": total, "pct": pct, "unmeasured": unmeasured},
|