@andresmassello/uscha 2.0.0 → 2.1.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 +1 -1
- package/package.json +1 -1
- package/uscha-kit/.claude/skills/uscha-devloop/SKILL.md +30 -7
- package/uscha-kit/.claude/skills/uscha-devloop/qa_ledger.py +148 -28
- package/uscha-kit/.claude-plugin/plugin.json +1 -1
- package/uscha-kit/.codex-plugin/plugin.json +1 -1
- package/uscha-kit/README.md +21 -7
- package/uscha-kit/VERSION +1 -1
- package/uscha-kit/skills/uscha-devloop/SKILL.md +30 -7
- package/uscha-kit/skills/uscha-devloop/qa_ledger.py +148 -28
- package/uscha-kit/templates/CONSTITUTION.md +10 -3
- package/uscha-kit/uscha.config.json +3 -2
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 v2.
|
|
43
|
+
**Kit v2.1.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
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andresmassello/uscha",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.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",
|
|
@@ -255,7 +255,7 @@ Implement per the PLAN. Commit per logical step with conventional commits
|
|
|
255
255
|
- **Never edit the SPEC/ADR to make the implementation look correct.** If reality forces
|
|
256
256
|
a change, amend the SPEC (version it) and return to Ready.
|
|
257
257
|
|
|
258
|
-
## Phase 2b — Simplicity
|
|
258
|
+
## Phase 2b — Simplicity check ("Reduce") — ADVISORY by default (kit 2.1.0)
|
|
259
259
|
|
|
260
260
|
Before the QA loop, check the change isn't overbuilt. This is the CONSTITUTION's
|
|
261
261
|
**Simplicidad** invariant made deterministic — diff minimality, nesting depth and new
|
|
@@ -266,11 +266,25 @@ git diff --unified=0 <base> | python3 $QL simplicity-check --config uscha.config
|
|
|
266
266
|
# or: python3 $QL simplicity-check --from-git --base <base>
|
|
267
267
|
```
|
|
268
268
|
|
|
269
|
-
Reads `SIMPLICITY: NN/100 — SIMPLE | ACCEPTABLE | OVERBUILT
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
269
|
+
Reads `SIMPLICITY: NN/100 — SIMPLE | ACCEPTABLE | OVERBUILT (advisory | declared gate)`.
|
|
270
|
+
|
|
271
|
+
**Advisory is the default and it exits 0** (ADR-043). Every budget is the KIT'S OPINION until
|
|
272
|
+
the project declares its own; an opinion that stops a loop is a gate nobody asked for. In
|
|
273
|
+
advisory mode an OVERBUILT verdict is **information for the human**: cut what is cheap to cut,
|
|
274
|
+
**report it in the PR body** with the score and the flags, and **never block on it** — do not
|
|
275
|
+
loop, do not refuse to converge, do not "fix" the diff to chase the number.
|
|
276
|
+
|
|
277
|
+
**It gates only when the project says so**: at least one declared budget in
|
|
278
|
+
`config.defaults.simplicity` **AND** `defaults.simplicity.gate: true` (or `--gate`). Then
|
|
279
|
+
OVERBUILT is exit 1 and a BLOCKER again: reduce first (guard clauses, drop speculative
|
|
280
|
+
types/layers, split giant hunks) and re-run. `gate: true` with **no** budget declared is a
|
|
281
|
+
config error, exit 2 — a gate with no budget is not a gate.
|
|
282
|
+
|
|
283
|
+
**`max_nesting` is an INDENTATION-DEPTH proxy, not AST nesting** — it counts leading
|
|
284
|
+
indentation on added lines. A wrapped call argument, JSX, or a multi-line Java/Kotlin literal
|
|
285
|
+
raises it with no control flow present at all, which is the single most common false OVERBUILT.
|
|
286
|
+
Discount it accordingly; the kit does not try to make it language-aware. For a 2-space codebase
|
|
287
|
+
pass `--indent-width 2`.
|
|
274
288
|
|
|
275
289
|
**Tests are OUTSIDE the budget** (kit 1.11.0): test files (the 9 stack conventions) are
|
|
276
290
|
counted and reported apart (`test_lines_added`) but never gate — writing tests must not
|
|
@@ -278,13 +292,22 @@ push a diff toward OVERBUILT (deleting them is already blocked by gate-check). A
|
|
|
278
292
|
project can have MORE test code than production code.
|
|
279
293
|
|
|
280
294
|
**Persist the verdict** so convergence and readiness see it (facts block through the
|
|
281
|
-
ledger, not through your goodwill):
|
|
295
|
+
ledger, not through your goodwill) — and persist it as what it WAS:
|
|
282
296
|
|
|
283
297
|
```bash
|
|
298
|
+
# advisory mode (the default): the run is recorded, and it caps nothing and blocks nothing
|
|
299
|
+
python3 $QL log-gate --repo <REPO> --iteration <N> --kind simplicity \
|
|
300
|
+
--verdict advisory [--note "OVERBUILT 58/100 — advisory, no budget declared"]
|
|
301
|
+
|
|
302
|
+
# declared gate only (defaults.simplicity.gate: true + budgets)
|
|
284
303
|
python3 $QL log-gate --repo <REPO> --iteration <N> --kind simplicity \
|
|
285
304
|
--verdict <pass|fail> [--note "OVERBUILT: +612 lines vs 400 budget"]
|
|
286
305
|
```
|
|
287
306
|
|
|
307
|
+
**Never log an advisory run as `pass`.** `pass` means a declared gate ran and came back clean;
|
|
308
|
+
an advisory run means there was no gate. Readiness prints them apart (`N ok · 1 advisory`) and
|
|
309
|
+
the mirador shows `ADVISORY` instead of `OK` — but only if you tell it the truth here.
|
|
310
|
+
|
|
288
311
|
### Phase 2c — REUSE-FIRST gate (kit 1.26.0)
|
|
289
312
|
|
|
290
313
|
Simplicity scores the diff in ISOLATION; it cannot see that the new block re-implements
|
|
@@ -44,7 +44,8 @@ Usage (see `--help` on each subcommand):
|
|
|
44
44
|
qa_ledger.py rebuild --mode baseline --config uscha.config.json [--out REBUILD-BASELINE.json]
|
|
45
45
|
qa_ledger.py rebuild --mode compare --baseline REBUILD-BASELINE.json [--json]
|
|
46
46
|
qa_ledger.py simplicity-check --diff changes.diff [--config uscha.config.json] [--json]
|
|
47
|
-
qa_ledger.py simplicity-check --from-git --base main
|
|
47
|
+
qa_ledger.py simplicity-check --from-git --base main (advisory: exit 0)
|
|
48
|
+
qa_ledger.py simplicity-check --from-git --base main --max-lines-added 400 --gate
|
|
48
49
|
qa_ledger.py pit-check --report target/pit-reports/*/mutations.xml [--min-score 60] [--json]
|
|
49
50
|
qa_ledger.py gate-check --from-git --base main [--strict] [--json]
|
|
50
51
|
qa_ledger.py spec-check --spec SPEC.md [--spec ACCEPTANCE.md] [--strict] [--json]
|
|
@@ -2497,6 +2498,11 @@ def _gate_rollup(ledger):
|
|
|
2497
2498
|
for tool, rec in _latest_static_by_tool(rnode).items():
|
|
2498
2499
|
gates.append({"repo": rname, "tool": tool,
|
|
2499
2500
|
"blocking": rec.get("gated_reported", 0) > 0,
|
|
2501
|
+
# ADR-043: an advisory record is non-blocking BY CONSTRUCTION, which
|
|
2502
|
+
# is not the same fact as a gate that ran and came back clean. It
|
|
2503
|
+
# travels so no consumer has to guess which of the two it is looking
|
|
2504
|
+
# at -- the false-clean is the failure mode, not the absence.
|
|
2505
|
+
"advisory": bool(rec.get("advisory")),
|
|
2500
2506
|
"gated": rec.get("gated_reported", 0),
|
|
2501
2507
|
"note": rec.get("note")})
|
|
2502
2508
|
return sorted(gates, key=lambda g: (g["repo"], g["tool"]))
|
|
@@ -2724,11 +2730,18 @@ def cmd_spec_change_request(args):
|
|
|
2724
2730
|
(row["id"], args.repo, args.source, args.requested_change))
|
|
2725
2731
|
|
|
2726
2732
|
|
|
2727
|
-
def _append_gate_record(ledger, node, repo, tool, iteration, failing, count, note
|
|
2733
|
+
def _append_gate_record(ledger, node, repo, tool, iteration, failing, count, note,
|
|
2734
|
+
advisory=False):
|
|
2728
2735
|
"""Append a static-gate-shaped record for a FACT gate so the EXISTING plumbing
|
|
2729
2736
|
sees it: _gate_open_and_sev feeds the BLOCKER/CRITICAL readiness cap (<=65) and
|
|
2730
2737
|
_converged refuses while the latest record for the tool is failing. A later
|
|
2731
|
-
clean record for the same tool clears it (latest-per-tool wins).
|
|
2738
|
+
clean record for the same tool clears it (latest-per-tool wins).
|
|
2739
|
+
|
|
2740
|
+
advisory=True (kit 2.1.0, ADR-043) records a MEASUREMENT that is not a gate: the record
|
|
2741
|
+
carries zero gated findings, so it can neither cap readiness nor block convergence, and it
|
|
2742
|
+
is stamped so no surface can render it as a clean gate either. That distinction is the whole
|
|
2743
|
+
point -- a check running advisory is not the same fact as a check running green, and a
|
|
2744
|
+
ledger that cannot tell them apart is the false-clean this flag exists to refuse."""
|
|
2732
2745
|
ledger["step_counter"] += 1
|
|
2733
2746
|
n = max(1, count) if failing else 0
|
|
2734
2747
|
rec = {
|
|
@@ -2740,19 +2753,38 @@ def _append_gate_record(ledger, node, repo, tool, iteration, failing, count, not
|
|
|
2740
2753
|
"tests_passed": None, "files_changed": 0,
|
|
2741
2754
|
"fingerprint": None, "finding_ids": None, "note": note,
|
|
2742
2755
|
}
|
|
2756
|
+
if advisory:
|
|
2757
|
+
rec["advisory"] = True
|
|
2743
2758
|
node["iterations"].append(rec)
|
|
2744
2759
|
ledger["steps"].append({"n": rec["n"], "at": rec["at"], "kind": "static-gate",
|
|
2745
2760
|
"repo": repo, "tool": tool, "iteration": iteration})
|
|
2746
2761
|
return rec
|
|
2747
2762
|
|
|
2748
2763
|
|
|
2764
|
+
# The only --kind values log-gate accepts with --verdict advisory (ADR-043): the checks whose
|
|
2765
|
+
# default mode IS advisory. Every other kind is a FACT gate and records pass/fail/not-run only.
|
|
2766
|
+
ADVISORY_CAPABLE_KINDS = ("simplicity", "waste")
|
|
2767
|
+
|
|
2768
|
+
|
|
2749
2769
|
def cmd_log_gate(args):
|
|
2750
2770
|
"""Persist a FACT-gate verdict (golden-diff / gate-check / pit-check / simplicity / regression)
|
|
2751
2771
|
into the ledger, so 'facts may block' is enforced by the engine, not by goodwill.
|
|
2752
|
-
fail
|
|
2753
|
-
pass
|
|
2754
|
-
|
|
2755
|
-
|
|
2772
|
+
fail -> BLOCKER record: trips the <=65 readiness cap AND blocks convergence.
|
|
2773
|
+
pass -> clean record for the same tool: credits the fix, convergence sees clean.
|
|
2774
|
+
advisory -> a MEASURED, non-gating record (kit 2.1.0, ADR-043): zero gated findings, so
|
|
2775
|
+
it can never cap readiness nor block convergence, and stamped `advisory` so
|
|
2776
|
+
no surface counts it as an `ok` gate. Use it for a check the project has not
|
|
2777
|
+
declared as a gate -- `simplicity-check` in its default advisory mode above
|
|
2778
|
+
all. The alternative (persisting an advisory as `pass`) is the false clean
|
|
2779
|
+
this verdict exists to refuse: a reader cannot tell "the gate was green"
|
|
2780
|
+
from "there was no gate", and the second is what actually happened.
|
|
2781
|
+
not-run -> a steps event ONLY, never an iterations record: absence is not
|
|
2782
|
+
evidence — it neither reads as clean nor fakes a red (last state stands).
|
|
2783
|
+
|
|
2784
|
+
The engine cannot observe which mode a SEPARATE `simplicity-check` process ran in, so this
|
|
2785
|
+
is a named verdict rather than a refusal: refusing would only be enforceable on trust,
|
|
2786
|
+
while a third verdict is enforceable on the ledger. The caller declares the mode; every
|
|
2787
|
+
reader downstream then sees it as a fact instead of inferring it.
|
|
2756
2788
|
"""
|
|
2757
2789
|
# INV-ADVISORY-01 note (ADR-014): --kind is a CLOSED vocabulary (argparse choices), so
|
|
2758
2790
|
# an advisory-class dimension (e.g. "semantic") cannot be registered as a gate through
|
|
@@ -2774,12 +2806,27 @@ def cmd_log_gate(args):
|
|
|
2774
2806
|
f"(no evidence — last logged state stands, absence is never green)")
|
|
2775
2807
|
return
|
|
2776
2808
|
failing = args.verdict == "fail"
|
|
2809
|
+
advisory = args.verdict == "advisory"
|
|
2810
|
+
if advisory and args.kind not in ADVISORY_CAPABLE_KINDS:
|
|
2811
|
+
# ADR-043 widens --verdict for the checks that RUN advisory by default. A FACT gate
|
|
2812
|
+
# (deleted tests, a lowered threshold, a golden drift) recorded as advisory would be
|
|
2813
|
+
# a mandatory gate cleared by goodwill -- the exact thing this ledger exists to refuse.
|
|
2814
|
+
print(f"[qa_ledger] log-gate: --verdict advisory is not accepted for --kind {args.kind}: "
|
|
2815
|
+
f"only {', '.join(ADVISORY_CAPABLE_KINDS)} run in an advisory mode; a FACT gate "
|
|
2816
|
+
f"records pass, fail or not-run", file=sys.stderr)
|
|
2817
|
+
sys.exit(2)
|
|
2777
2818
|
rec = _append_gate_record(ledger, node, args.repo, tool, args.iteration,
|
|
2778
|
-
failing, args.count, args.note)
|
|
2819
|
+
failing, args.count, args.note, advisory=advisory)
|
|
2779
2820
|
_save(args.ledger, ledger)
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2821
|
+
if advisory:
|
|
2822
|
+
state, effect = "ADVISORY (measured, not gating)", (
|
|
2823
|
+
"reported everywhere as advisory, never as ok; caps nothing, blocks nothing")
|
|
2824
|
+
elif failing:
|
|
2825
|
+
state, effect = (f"FAIL (BLOCKER x{rec['gated_reported']})",
|
|
2826
|
+
"caps readiness <=65 and blocks convergence")
|
|
2827
|
+
else:
|
|
2828
|
+
state, effect = "PASS (clean)", "clears the gate for convergence"
|
|
2829
|
+
print(f"[qa_ledger] {args.repo}/{tool}: {state} logged — {effect}")
|
|
2783
2830
|
|
|
2784
2831
|
|
|
2785
2832
|
def cmd_flag_blocker(args):
|
|
@@ -8399,17 +8446,21 @@ def cmd_dashboard(args):
|
|
|
8399
8446
|
subscores = [{"k": "coverage",
|
|
8400
8447
|
"val": round(covp) if isinstance(covp, (int, float)) else None,
|
|
8401
8448
|
"bd": (f"{round(covp)}%" if isinstance(covp, (int, float)) else None)}]
|
|
8402
|
-
gate_block, gate_note = {}, {}
|
|
8449
|
+
gate_block, gate_note, gate_adv = {}, {}, {}
|
|
8403
8450
|
for g in rd.get("gates", []):
|
|
8404
8451
|
kind = (g.get("tool") or "").replace("gate:", "")
|
|
8405
8452
|
key = "golden" if kind.startswith("golden") else kind
|
|
8406
8453
|
gate_block[key] = gate_block.get(key, False) or bool(g.get("blocking"))
|
|
8454
|
+
gate_adv[key] = gate_adv.get(key, False) or bool(g.get("advisory"))
|
|
8407
8455
|
if g.get("note") and key not in gate_note:
|
|
8408
8456
|
gate_note[key] = g.get("note")
|
|
8409
8457
|
for key in ("simplicity", "waste", "golden"):
|
|
8410
8458
|
if key in gate_block:
|
|
8459
|
+
# ADR-043: a non-blocking ADVISORY is not "OK" — OK means a declared gate ran clean.
|
|
8460
|
+
_bd = ("FAIL" if gate_block[key]
|
|
8461
|
+
else "ADVISORY" if gate_adv.get(key) else "OK")
|
|
8411
8462
|
subscores.append({"k": key, "val": None,
|
|
8412
|
-
"bd": gate_note.get(key) or
|
|
8463
|
+
"bd": gate_note.get(key) or _bd})
|
|
8413
8464
|
|
|
8414
8465
|
# loops: iters + estado por repo (escalated > converged > active). max sin fuente.
|
|
8415
8466
|
# El estado se deriva ENTERO con _derive_phase (kit 1.48.1) — la MISMA funcion que
|
|
@@ -9033,6 +9084,10 @@ def _top_events(ledger, limit=TOP_EVENTS_TAIL):
|
|
|
9033
9084
|
gated = it.get("gated_reported")
|
|
9034
9085
|
if kind == "gate-not-run":
|
|
9035
9086
|
tail = "not run — nobody measured it"
|
|
9087
|
+
elif kind == "static-gate" and it.get("advisory"):
|
|
9088
|
+
# ADR-043: measured but not gating. `info` (never green, never red) is the
|
|
9089
|
+
# honest level -- rendering it `pass`/`clean` is the false clean again.
|
|
9090
|
+
level, tail = "info", "advisory — measured, not gating"
|
|
9036
9091
|
elif kind == "static-gate" and isinstance(gated, int):
|
|
9037
9092
|
level = "fail" if gated >= 1 else "pass"
|
|
9038
9093
|
tail = "%d gated finding(s)" % gated if gated else "clean"
|
|
@@ -9957,13 +10012,21 @@ def cmd_readiness(args):
|
|
|
9957
10012
|
gate_roll = out["gates"]
|
|
9958
10013
|
if gate_roll:
|
|
9959
10014
|
blocking = [g for g in gate_roll if g["blocking"]]
|
|
9960
|
-
|
|
10015
|
+
# ADR-043: an advisory NEVER joins the ok count. "3 ok" must mean three gates ran and
|
|
10016
|
+
# came back clean; folding a check the project never declared as a gate into that number
|
|
10017
|
+
# is the false clean the advisory verdict exists to refuse. The segment is conditional,
|
|
10018
|
+
# so a ledger with no advisory record prints exactly what it printed before.
|
|
10019
|
+
advisory = [g for g in gate_roll if g.get("advisory") and not g["blocking"]]
|
|
10020
|
+
n_ok = len(gate_roll) - len(blocking) - len(advisory)
|
|
10021
|
+
adv_str = (f" · {len(advisory)} advisory ("
|
|
10022
|
+
+ ", ".join(f"{g['repo']}/{g['tool']}" for g in advisory) + ")"
|
|
10023
|
+
if advisory else "")
|
|
9961
10024
|
hint = "" if args.verbose else " (readiness --verbose for the detail)"
|
|
9962
10025
|
if blocking:
|
|
9963
10026
|
names = ", ".join(f"{g['repo']}/{g['tool']}" for g in blocking)
|
|
9964
|
-
print(f"--- gates: {n_ok} ok · {len(blocking)} blocking ({names}){hint}")
|
|
10027
|
+
print(f"--- gates: {n_ok} ok{adv_str} · {len(blocking)} blocking ({names}){hint}")
|
|
9965
10028
|
else:
|
|
9966
|
-
print(f"--- gates: {n_ok} ok, none blocking{hint}")
|
|
10029
|
+
print(f"--- gates: {n_ok} ok{adv_str}, none blocking{hint}")
|
|
9967
10030
|
if not args.verbose:
|
|
9968
10031
|
return
|
|
9969
10032
|
print("--- dimensions (weight | raw | contribution) ---")
|
|
@@ -10023,6 +10086,14 @@ DEFAULT_COVERAGE_TOLERANCE = 5.0 # pct points the rebuilt coverage may drop
|
|
|
10023
10086
|
# abstraction is INTENTIONALLY not weighted: the "new types" regex is a prose/AST proxy
|
|
10024
10087
|
# that false-positives on Java records/DTOs, so it must not gate the band. It stays as an
|
|
10025
10088
|
# advisory metric + flag only (distilled: hard caps gate, guessy proxies advise).
|
|
10089
|
+
#
|
|
10090
|
+
# ADVISORY BY DEFAULT (kit 2.1.0, ADR-043). Every budget below is the KIT'S OPINION, not the
|
|
10091
|
+
# project's requirement, and an opinion that exits 1 is a gate nobody declared. Until a project
|
|
10092
|
+
# declares at least one numeric budget AND `defaults.simplicity.gate: true`, the verdict is
|
|
10093
|
+
# reported and the exit code is 0. This is NOT INV-ADVISORY-01 (that invariant quarantines
|
|
10094
|
+
# LLM-class JUDGMENT; these proxies are deterministic and may gate the moment a human says so)
|
|
10095
|
+
# -- it is the provenance rule of 1.17.0 applied to an exit code: a default is an opinion, and
|
|
10096
|
+
# only a declaration is a requirement.
|
|
10026
10097
|
SIMPLICITY_WEIGHTS = {
|
|
10027
10098
|
"diff_size": 35, "nesting": 30, "net_growth": 20, "fan_out": 8, "blob": 7,
|
|
10028
10099
|
}
|
|
@@ -10037,6 +10108,17 @@ SIMPLICITY_DEFAULTS = {
|
|
|
10037
10108
|
"max_abstraction_density": 3.0, # new *types* per 100 added LOC
|
|
10038
10109
|
"indent_width": 4,
|
|
10039
10110
|
}
|
|
10111
|
+
# Keys under defaults.simplicity that are NOT budgets, so declaring one never satisfies the
|
|
10112
|
+
# "a gate needs a budget" rule: `indent_width` is a PARSING parameter and `gate` is the switch
|
|
10113
|
+
# itself. `gate: true` with nothing but these declared is a refusal, not a gate (ADR-043).
|
|
10114
|
+
_SIMPLICITY_NON_BUDGET = ("indent_width", "gate")
|
|
10115
|
+
# What `max_nesting` actually measures, said once and reused by every surface that prints it.
|
|
10116
|
+
# It is INDENTATION DEPTH over added lines, not AST nesting: a wrapped call argument, JSX, a
|
|
10117
|
+
# multi-line Java string or any deep continuation raises it without any control flow existing.
|
|
10118
|
+
# The kit does NOT make it language-aware (that needs a parser per stack, which this stdlib
|
|
10119
|
+
# engine will not have) -- it names the proxy instead, so a reader can discount it.
|
|
10120
|
+
_NESTING_PROXY_NOTE = ("indentation depth over added lines, NOT AST nesting -- continuation "
|
|
10121
|
+
"lines, JSX and multi-line literals inflate it")
|
|
10040
10122
|
# code files only — docs, config, resources and generated trees are noise for a
|
|
10041
10123
|
# code-simplicity gate. Broader than SOURCE_EXT (which is repo-typed for rebuild).
|
|
10042
10124
|
_SIMPLICITY_CODE_EXT = {
|
|
@@ -10449,8 +10531,9 @@ def _simplicity_score(m, b):
|
|
|
10449
10531
|
def _simplicity_flags(m, b):
|
|
10450
10532
|
f = []
|
|
10451
10533
|
if m["max_nesting"] > b["max_nesting_depth"]:
|
|
10452
|
-
f.append(f"
|
|
10453
|
-
f"aplanar: guard clauses / extraer función
|
|
10534
|
+
f.append(f"max_nesting (indentation proxy) {m['max_nesting']} > "
|
|
10535
|
+
f"{b['max_nesting_depth']} — aplanar: guard clauses / extraer función "
|
|
10536
|
+
f"(CWE-1124). Proxy: {_NESTING_PROXY_NOTE}")
|
|
10454
10537
|
if m["new_abstractions"] > b["max_new_abstractions"]:
|
|
10455
10538
|
f.append(f"{m['new_abstractions']} tipos/capas nuevos > "
|
|
10456
10539
|
f"{b['max_new_abstractions']} — ¿todos pedidos? "
|
|
@@ -10476,10 +10559,12 @@ def _simplicity_flags(m, b):
|
|
|
10476
10559
|
def cmd_simplicity_check(args):
|
|
10477
10560
|
b = dict(SIMPLICITY_DEFAULTS)
|
|
10478
10561
|
declared = set() # presupuestos declarados por el humano (config o CLI)
|
|
10562
|
+
gate = False # ADR-043: solo lo enciende una DECLARACION, nunca un default
|
|
10479
10563
|
if args.config and os.path.exists(args.config):
|
|
10480
10564
|
cfg = _load(args.config).get("defaults", {}).get("simplicity", {})
|
|
10481
10565
|
b.update({k: cfg[k] for k in b if k in cfg})
|
|
10482
|
-
declared |= {k for k in b if k in cfg and k
|
|
10566
|
+
declared |= {k for k in b if k in cfg and k not in _SIMPLICITY_NON_BUDGET}
|
|
10567
|
+
gate = bool(cfg.get("gate"))
|
|
10483
10568
|
for k in ("max_lines_added", "max_net_lines", "max_files_changed",
|
|
10484
10569
|
"max_nesting_depth", "max_hunk_added", "max_new_abstractions",
|
|
10485
10570
|
"indent_width"):
|
|
@@ -10491,34 +10576,55 @@ def cmd_simplicity_check(args):
|
|
|
10491
10576
|
if args.max_abstraction_density is not None:
|
|
10492
10577
|
b["max_abstraction_density"] = args.max_abstraction_density
|
|
10493
10578
|
declared.add("max_abstraction_density")
|
|
10579
|
+
if getattr(args, "gate", False):
|
|
10580
|
+
gate = True
|
|
10581
|
+
# A gate with no budget is not a gate: it is the kit's opinion wearing an exit code, which
|
|
10582
|
+
# is exactly the defect ADR-043 exists to remove. Refuse BEFORE reading the diff -- a
|
|
10583
|
+
# misconfigured gate must not produce a score anyone could quote.
|
|
10584
|
+
if gate and not declared:
|
|
10585
|
+
print("[qa_ledger] invalid config: defaults.simplicity.gate is true (or --gate was "
|
|
10586
|
+
"passed) but no simplicity budget is declared — a gate with no budget is not a "
|
|
10587
|
+
"gate, only the kit's opinion with an exit code. Declare at least one of "
|
|
10588
|
+
"max_lines_added, max_net_lines, max_files_changed, max_nesting_depth, "
|
|
10589
|
+
"max_hunk_added, max_new_abstractions, max_abstraction_density in "
|
|
10590
|
+
"defaults.simplicity (or pass the matching --max-... flag), or set gate to false.",
|
|
10591
|
+
file=sys.stderr)
|
|
10592
|
+
sys.exit(2)
|
|
10593
|
+
mode = "gate" if gate else "advisory"
|
|
10494
10594
|
|
|
10495
10595
|
m = _simplicity_metrics(_read_diff(args), b["indent_width"])
|
|
10496
10596
|
score, dims = _simplicity_score(m, b)
|
|
10497
10597
|
verdict = _simplicity_band(score)
|
|
10498
10598
|
flags = _simplicity_flags(m, b)
|
|
10599
|
+
exit_code = 1 if (verdict == "OVERBUILT" and gate) else 0
|
|
10499
10600
|
|
|
10500
|
-
out = {"score": score, "verdict": verdict, "
|
|
10601
|
+
out = {"score": score, "verdict": verdict, "mode": mode, "gate": gate,
|
|
10602
|
+
"weights": SIMPLICITY_WEIGHTS,
|
|
10501
10603
|
"dimensions": {k: round(v, 3) for k, v in dims.items()},
|
|
10502
|
-
"metrics": m, "
|
|
10604
|
+
"metrics": m, "metrics_notes": {"max_nesting": _NESTING_PROXY_NOTE},
|
|
10605
|
+
"budgets": b, "budgets_declared": sorted(declared),
|
|
10503
10606
|
"flags": flags}
|
|
10504
10607
|
if args.json:
|
|
10505
10608
|
print(json.dumps(out, indent=2, ensure_ascii=False))
|
|
10506
|
-
sys.exit(
|
|
10609
|
+
sys.exit(exit_code)
|
|
10507
10610
|
|
|
10508
|
-
|
|
10611
|
+
mode_str = ("declared gate" if gate else
|
|
10612
|
+
"advisory (declare budgets + defaults.simplicity.gate to make it block)")
|
|
10613
|
+
print(f"SIMPLICITY: {score}/100 — {verdict} ({mode_str})")
|
|
10509
10614
|
print("--- metrics (value / budget · * = declared by the human) ---")
|
|
10510
10615
|
rows = [
|
|
10511
10616
|
("lines_added", m["lines_added"], b["max_lines_added"], "max_lines_added"),
|
|
10512
10617
|
("net_lines", m["net_lines"], b["max_net_lines"], "max_net_lines"),
|
|
10513
10618
|
("files_changed", m["files_changed"], b["max_files_changed"], "max_files_changed"),
|
|
10514
|
-
("max_nesting", m["max_nesting"], b["max_nesting_depth"], "max_nesting_depth"),
|
|
10619
|
+
("max_nesting (indentation proxy)", m["max_nesting"], b["max_nesting_depth"], "max_nesting_depth"),
|
|
10515
10620
|
("new_abstractions", m["new_abstractions"], b["max_new_abstractions"], "max_new_abstractions"),
|
|
10516
10621
|
("abstraction/100", m["abstraction_density"], b["max_abstraction_density"], "max_abstraction_density"),
|
|
10517
10622
|
("max_hunk_added", m["max_hunk_added"], b["max_hunk_added"], "max_hunk_added"),
|
|
10518
10623
|
]
|
|
10519
10624
|
for name, val, bud, key in rows:
|
|
10520
10625
|
mark = "*" if key in declared else ""
|
|
10521
|
-
print(f" {name:
|
|
10626
|
+
print(f" {name:31s} {str(val):>7s} / {bud}{mark}")
|
|
10627
|
+
print(f" (max_nesting is a PROXY: {_NESTING_PROXY_NOTE})")
|
|
10522
10628
|
if not declared:
|
|
10523
10629
|
print(" (every budget is a kit default — an opinion, not a "
|
|
10524
10630
|
"requirement: declare yours in config.defaults.simplicity)")
|
|
@@ -10535,7 +10641,11 @@ def cmd_simplicity_check(args):
|
|
|
10535
10641
|
print(f" ! {fl}")
|
|
10536
10642
|
else:
|
|
10537
10643
|
print(" within budget — nothing to cut")
|
|
10538
|
-
|
|
10644
|
+
if verdict == "OVERBUILT" and not gate:
|
|
10645
|
+
print("--- advisory: OVERBUILT is REPORTED, not enforced (exit 0). Cut what is cheap, "
|
|
10646
|
+
"say so in the PR body, and do not let it block the loop. To make it block, "
|
|
10647
|
+
"declare your budgets AND defaults.simplicity.gate: true ---")
|
|
10648
|
+
sys.exit(exit_code)
|
|
10539
10649
|
|
|
10540
10650
|
|
|
10541
10651
|
# --------------------------------------------------------------------------- #
|
|
@@ -12876,7 +12986,11 @@ def build_parser():
|
|
|
12876
12986
|
plg.add_argument("--kind", required=True,
|
|
12877
12987
|
choices=["golden-diff", "gate-check", "pit-check", "simplicity",
|
|
12878
12988
|
"regression", "rubric", "waste"])
|
|
12879
|
-
plg.add_argument("--verdict", required=True,
|
|
12989
|
+
plg.add_argument("--verdict", required=True,
|
|
12990
|
+
choices=["pass", "fail", "advisory", "not-run"],
|
|
12991
|
+
help="advisory (ADR-043) records a measured, non-gating run: it never "
|
|
12992
|
+
"caps readiness, never blocks convergence, and never reads as ok; "
|
|
12993
|
+
"accepted only for --kind simplicity|waste, a FACT gate refuses it")
|
|
12880
12994
|
plg.add_argument("--count", type=int, default=1,
|
|
12881
12995
|
help="failing finding count (fail only; default 1)")
|
|
12882
12996
|
plg.add_argument("--note", default=None)
|
|
@@ -13057,7 +13171,9 @@ def build_parser():
|
|
|
13057
13171
|
|
|
13058
13172
|
ps2 = sub.add_parser(
|
|
13059
13173
|
"simplicity-check",
|
|
13060
|
-
help="Reduce gate: score diff minimality/complexity over a unified diff"
|
|
13174
|
+
help="Reduce gate: score diff minimality/complexity over a unified diff. Advisory "
|
|
13175
|
+
"by default; gates only with --gate or defaults.simplicity.gate AND at least "
|
|
13176
|
+
"one declared budget")
|
|
13061
13177
|
ps2.add_argument("--diff", help="path to a unified diff (else --from-git or stdin)")
|
|
13062
13178
|
ps2.add_argument("--from-git", action="store_true",
|
|
13063
13179
|
help="run `git diff --unified=0 <base>` for the diff")
|
|
@@ -13073,6 +13189,10 @@ def build_parser():
|
|
|
13073
13189
|
ps2.add_argument("--max-abstraction-density", dest="max_abstraction_density",
|
|
13074
13190
|
type=float, default=None)
|
|
13075
13191
|
ps2.add_argument("--indent-width", dest="indent_width", type=int)
|
|
13192
|
+
ps2.add_argument("--gate", action="store_true",
|
|
13193
|
+
help="make an OVERBUILT verdict exit 1 (same switch as "
|
|
13194
|
+
"defaults.simplicity.gate). Requires at least one declared budget: "
|
|
13195
|
+
"without one the run REFUSES with exit 2 (ADR-043)")
|
|
13076
13196
|
ps2.add_argument("--json", action="store_true")
|
|
13077
13197
|
ps2.set_defaults(func=cmd_simplicity_check)
|
|
13078
13198
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "uscha",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.1.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, 53 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:** v2.
|
|
3
|
+
**Kit version:** v2.1.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`,
|
|
@@ -621,9 +621,9 @@ python3 $QL rebuild --mode compare --baseline REBUILD-BASELINE.json --json # c
|
|
|
621
621
|
- Verdicts: `COVERS ≥90` · `PARTIAL ≥70` · `DIVERGE <70`. The score lists the concrete
|
|
622
622
|
**gaps** — feed them back into the SPEC and re-run. Divergence is a spec hole, not a code bug.
|
|
623
623
|
|
|
624
|
-
## Simplicity
|
|
624
|
+
## Simplicity check — "Reduce" (minimality of the change) — ADVISORY by default
|
|
625
625
|
|
|
626
|
-
The **Simplicity** invariant of the CONSTITUTION made
|
|
626
|
+
The **Simplicity** invariant of the CONSTITUTION made deterministic: it scores the *diff*
|
|
627
627
|
(not CC by AST — they are measurable proxies: minimality, nesting, new abstractions).
|
|
628
628
|
|
|
629
629
|
```bash
|
|
@@ -634,21 +634,35 @@ python3 $QL simplicity-check --diff changes.diff --json # consumed by usc
|
|
|
634
634
|
|
|
635
635
|
- Dimensions/weights: diff_size 35, nesting 30, net_growth 20, fan_out 8, blob 7
|
|
636
636
|
(abstraction does NOT weigh in the score — it's a guessy proxy, kept as a metric + advisory flag).
|
|
637
|
-
- Verdicts: `SIMPLE ≥85` · `ACCEPTABLE ≥65` · `OVERBUILT <65
|
|
637
|
+
- Verdicts: `SIMPLE ≥85` · `ACCEPTABLE ≥65` · `OVERBUILT <65`.
|
|
638
638
|
A gross excess (2× budget, or very deep nesting) caps the score at 60 no matter what.
|
|
639
|
+
- **Advisory by default, exit 0** (kit 2.1.0, ADR-043): every budget above is the KIT's opinion
|
|
640
|
+
until you declare your own, and an opinion that exits 1 is a gate nobody asked for. It
|
|
641
|
+
**gates** — OVERBUILT = exit 1 = BLOCKER — only with at least one budget declared in
|
|
642
|
+
`defaults.simplicity` **AND** `defaults.simplicity.gate: true` (or `--gate`). `gate: true`
|
|
643
|
+
with no budget declared is a config error, exit 2: a gate with no budget is not a gate.
|
|
644
|
+
`log-gate --kind simplicity --verdict advisory` persists an advisory run as an advisory —
|
|
645
|
+
readiness prints `N ok · 1 advisory` and the mirador reads `ADVISORY`, never `OK`.
|
|
646
|
+
- **`max_nesting` is an INDENTATION-DEPTH proxy**, not AST nesting: it reads leading
|
|
647
|
+
indentation on added lines, so a wrapped argument, JSX, or a multi-line Java literal inflates
|
|
648
|
+
it with no control flow present. It is named as a proxy in the report rather than made
|
|
649
|
+
language-aware. 2-space codebase → `--indent-width 2`.
|
|
639
650
|
- **Tests OUT of the budget** (kit 1.11.0): the test files (conventions of the
|
|
640
651
|
9 stacks) are counted and reported separately (`test_lines_added`) but do not gate — writing
|
|
641
652
|
tests never pushes the diff to OVERBUILT (deleting them is already blocked by gate-check).
|
|
642
653
|
- The flags tell you what to trim (guard clauses, speculative types/layers, giant hunks).
|
|
643
|
-
- Budgets in `defaults.simplicity
|
|
654
|
+
- Budgets in `defaults.simplicity`. No risk profile owns them: the gate is a human declaration
|
|
655
|
+
under every profile A–E.
|
|
644
656
|
|
|
645
657
|
## Ledger subcommands
|
|
646
658
|
|
|
647
659
|
`bench - bench-curate - bench-r2 - bench-roundtrip - bootstrap-oracle - bootstrap-variance - check-coverage - check-terminado - cleanroom - compile-ingest - compile-validate - converged - curate - curation-check - dashboard - discover - doctor - escalate - execution-policy - facts - fastpath-eval - fidelity - flag-blocker - gate-check - golden-coverage - golden-diff - ingest-gate - init - ir-extract - ir-render - lang-compare - log-gate - log-step - oscillation - phase - pit-check - production-finding - promote - readiness - rebuild - regression-check - resolve-escalation - roundtrip - rubric-ingest - simplicity-check - snapshot - spec-change-request - spec-check - spec-doubt - spec-drift - summary - top - waste-check` - the exact current `qa_ledger.py` parser surface (53 subcommands, derived from `SYSTEM-FACTS.json`, itself introspected from `build_parser()`); each supports `--help`.
|
|
648
660
|
|
|
649
661
|
The **fact gates** (golden-diff, gate-check, pit-check, simplicity) are PERSISTED with
|
|
650
|
-
`log-gate`: a fail blocks convergence and caps readiness ≤65 via the ledger
|
|
651
|
-
|
|
662
|
+
`log-gate`: a fail blocks convergence and caps readiness ≤65 via the ledger; `--verdict
|
|
663
|
+
advisory` records a run that is measured but not gating (it caps nothing, blocks nothing, and
|
|
664
|
+
never counts as an `ok` gate). A CONSTITUTION violation is recorded with `flag-blocker` (same
|
|
665
|
+
effect, until `--resolve`).
|
|
652
666
|
|
|
653
667
|
## Notes
|
|
654
668
|
|
package/uscha-kit/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
uscha-kit 2.
|
|
1
|
+
uscha-kit 2.1.0
|
|
@@ -255,7 +255,7 @@ Implement per the PLAN. Commit per logical step with conventional commits
|
|
|
255
255
|
- **Never edit the SPEC/ADR to make the implementation look correct.** If reality forces
|
|
256
256
|
a change, amend the SPEC (version it) and return to Ready.
|
|
257
257
|
|
|
258
|
-
## Phase 2b — Simplicity
|
|
258
|
+
## Phase 2b — Simplicity check ("Reduce") — ADVISORY by default (kit 2.1.0)
|
|
259
259
|
|
|
260
260
|
Before the QA loop, check the change isn't overbuilt. This is the CONSTITUTION's
|
|
261
261
|
**Simplicidad** invariant made deterministic — diff minimality, nesting depth and new
|
|
@@ -266,11 +266,25 @@ git diff --unified=0 <base> | python3 $QL simplicity-check --config uscha.config
|
|
|
266
266
|
# or: python3 $QL simplicity-check --from-git --base <base>
|
|
267
267
|
```
|
|
268
268
|
|
|
269
|
-
Reads `SIMPLICITY: NN/100 — SIMPLE | ACCEPTABLE | OVERBUILT
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
269
|
+
Reads `SIMPLICITY: NN/100 — SIMPLE | ACCEPTABLE | OVERBUILT (advisory | declared gate)`.
|
|
270
|
+
|
|
271
|
+
**Advisory is the default and it exits 0** (ADR-043). Every budget is the KIT'S OPINION until
|
|
272
|
+
the project declares its own; an opinion that stops a loop is a gate nobody asked for. In
|
|
273
|
+
advisory mode an OVERBUILT verdict is **information for the human**: cut what is cheap to cut,
|
|
274
|
+
**report it in the PR body** with the score and the flags, and **never block on it** — do not
|
|
275
|
+
loop, do not refuse to converge, do not "fix" the diff to chase the number.
|
|
276
|
+
|
|
277
|
+
**It gates only when the project says so**: at least one declared budget in
|
|
278
|
+
`config.defaults.simplicity` **AND** `defaults.simplicity.gate: true` (or `--gate`). Then
|
|
279
|
+
OVERBUILT is exit 1 and a BLOCKER again: reduce first (guard clauses, drop speculative
|
|
280
|
+
types/layers, split giant hunks) and re-run. `gate: true` with **no** budget declared is a
|
|
281
|
+
config error, exit 2 — a gate with no budget is not a gate.
|
|
282
|
+
|
|
283
|
+
**`max_nesting` is an INDENTATION-DEPTH proxy, not AST nesting** — it counts leading
|
|
284
|
+
indentation on added lines. A wrapped call argument, JSX, or a multi-line Java/Kotlin literal
|
|
285
|
+
raises it with no control flow present at all, which is the single most common false OVERBUILT.
|
|
286
|
+
Discount it accordingly; the kit does not try to make it language-aware. For a 2-space codebase
|
|
287
|
+
pass `--indent-width 2`.
|
|
274
288
|
|
|
275
289
|
**Tests are OUTSIDE the budget** (kit 1.11.0): test files (the 9 stack conventions) are
|
|
276
290
|
counted and reported apart (`test_lines_added`) but never gate — writing tests must not
|
|
@@ -278,13 +292,22 @@ push a diff toward OVERBUILT (deleting them is already blocked by gate-check). A
|
|
|
278
292
|
project can have MORE test code than production code.
|
|
279
293
|
|
|
280
294
|
**Persist the verdict** so convergence and readiness see it (facts block through the
|
|
281
|
-
ledger, not through your goodwill):
|
|
295
|
+
ledger, not through your goodwill) — and persist it as what it WAS:
|
|
282
296
|
|
|
283
297
|
```bash
|
|
298
|
+
# advisory mode (the default): the run is recorded, and it caps nothing and blocks nothing
|
|
299
|
+
python3 $QL log-gate --repo <REPO> --iteration <N> --kind simplicity \
|
|
300
|
+
--verdict advisory [--note "OVERBUILT 58/100 — advisory, no budget declared"]
|
|
301
|
+
|
|
302
|
+
# declared gate only (defaults.simplicity.gate: true + budgets)
|
|
284
303
|
python3 $QL log-gate --repo <REPO> --iteration <N> --kind simplicity \
|
|
285
304
|
--verdict <pass|fail> [--note "OVERBUILT: +612 lines vs 400 budget"]
|
|
286
305
|
```
|
|
287
306
|
|
|
307
|
+
**Never log an advisory run as `pass`.** `pass` means a declared gate ran and came back clean;
|
|
308
|
+
an advisory run means there was no gate. Readiness prints them apart (`N ok · 1 advisory`) and
|
|
309
|
+
the mirador shows `ADVISORY` instead of `OK` — but only if you tell it the truth here.
|
|
310
|
+
|
|
288
311
|
### Phase 2c — REUSE-FIRST gate (kit 1.26.0)
|
|
289
312
|
|
|
290
313
|
Simplicity scores the diff in ISOLATION; it cannot see that the new block re-implements
|
|
@@ -44,7 +44,8 @@ Usage (see `--help` on each subcommand):
|
|
|
44
44
|
qa_ledger.py rebuild --mode baseline --config uscha.config.json [--out REBUILD-BASELINE.json]
|
|
45
45
|
qa_ledger.py rebuild --mode compare --baseline REBUILD-BASELINE.json [--json]
|
|
46
46
|
qa_ledger.py simplicity-check --diff changes.diff [--config uscha.config.json] [--json]
|
|
47
|
-
qa_ledger.py simplicity-check --from-git --base main
|
|
47
|
+
qa_ledger.py simplicity-check --from-git --base main (advisory: exit 0)
|
|
48
|
+
qa_ledger.py simplicity-check --from-git --base main --max-lines-added 400 --gate
|
|
48
49
|
qa_ledger.py pit-check --report target/pit-reports/*/mutations.xml [--min-score 60] [--json]
|
|
49
50
|
qa_ledger.py gate-check --from-git --base main [--strict] [--json]
|
|
50
51
|
qa_ledger.py spec-check --spec SPEC.md [--spec ACCEPTANCE.md] [--strict] [--json]
|
|
@@ -2497,6 +2498,11 @@ def _gate_rollup(ledger):
|
|
|
2497
2498
|
for tool, rec in _latest_static_by_tool(rnode).items():
|
|
2498
2499
|
gates.append({"repo": rname, "tool": tool,
|
|
2499
2500
|
"blocking": rec.get("gated_reported", 0) > 0,
|
|
2501
|
+
# ADR-043: an advisory record is non-blocking BY CONSTRUCTION, which
|
|
2502
|
+
# is not the same fact as a gate that ran and came back clean. It
|
|
2503
|
+
# travels so no consumer has to guess which of the two it is looking
|
|
2504
|
+
# at -- the false-clean is the failure mode, not the absence.
|
|
2505
|
+
"advisory": bool(rec.get("advisory")),
|
|
2500
2506
|
"gated": rec.get("gated_reported", 0),
|
|
2501
2507
|
"note": rec.get("note")})
|
|
2502
2508
|
return sorted(gates, key=lambda g: (g["repo"], g["tool"]))
|
|
@@ -2724,11 +2730,18 @@ def cmd_spec_change_request(args):
|
|
|
2724
2730
|
(row["id"], args.repo, args.source, args.requested_change))
|
|
2725
2731
|
|
|
2726
2732
|
|
|
2727
|
-
def _append_gate_record(ledger, node, repo, tool, iteration, failing, count, note
|
|
2733
|
+
def _append_gate_record(ledger, node, repo, tool, iteration, failing, count, note,
|
|
2734
|
+
advisory=False):
|
|
2728
2735
|
"""Append a static-gate-shaped record for a FACT gate so the EXISTING plumbing
|
|
2729
2736
|
sees it: _gate_open_and_sev feeds the BLOCKER/CRITICAL readiness cap (<=65) and
|
|
2730
2737
|
_converged refuses while the latest record for the tool is failing. A later
|
|
2731
|
-
clean record for the same tool clears it (latest-per-tool wins).
|
|
2738
|
+
clean record for the same tool clears it (latest-per-tool wins).
|
|
2739
|
+
|
|
2740
|
+
advisory=True (kit 2.1.0, ADR-043) records a MEASUREMENT that is not a gate: the record
|
|
2741
|
+
carries zero gated findings, so it can neither cap readiness nor block convergence, and it
|
|
2742
|
+
is stamped so no surface can render it as a clean gate either. That distinction is the whole
|
|
2743
|
+
point -- a check running advisory is not the same fact as a check running green, and a
|
|
2744
|
+
ledger that cannot tell them apart is the false-clean this flag exists to refuse."""
|
|
2732
2745
|
ledger["step_counter"] += 1
|
|
2733
2746
|
n = max(1, count) if failing else 0
|
|
2734
2747
|
rec = {
|
|
@@ -2740,19 +2753,38 @@ def _append_gate_record(ledger, node, repo, tool, iteration, failing, count, not
|
|
|
2740
2753
|
"tests_passed": None, "files_changed": 0,
|
|
2741
2754
|
"fingerprint": None, "finding_ids": None, "note": note,
|
|
2742
2755
|
}
|
|
2756
|
+
if advisory:
|
|
2757
|
+
rec["advisory"] = True
|
|
2743
2758
|
node["iterations"].append(rec)
|
|
2744
2759
|
ledger["steps"].append({"n": rec["n"], "at": rec["at"], "kind": "static-gate",
|
|
2745
2760
|
"repo": repo, "tool": tool, "iteration": iteration})
|
|
2746
2761
|
return rec
|
|
2747
2762
|
|
|
2748
2763
|
|
|
2764
|
+
# The only --kind values log-gate accepts with --verdict advisory (ADR-043): the checks whose
|
|
2765
|
+
# default mode IS advisory. Every other kind is a FACT gate and records pass/fail/not-run only.
|
|
2766
|
+
ADVISORY_CAPABLE_KINDS = ("simplicity", "waste")
|
|
2767
|
+
|
|
2768
|
+
|
|
2749
2769
|
def cmd_log_gate(args):
|
|
2750
2770
|
"""Persist a FACT-gate verdict (golden-diff / gate-check / pit-check / simplicity / regression)
|
|
2751
2771
|
into the ledger, so 'facts may block' is enforced by the engine, not by goodwill.
|
|
2752
|
-
fail
|
|
2753
|
-
pass
|
|
2754
|
-
|
|
2755
|
-
|
|
2772
|
+
fail -> BLOCKER record: trips the <=65 readiness cap AND blocks convergence.
|
|
2773
|
+
pass -> clean record for the same tool: credits the fix, convergence sees clean.
|
|
2774
|
+
advisory -> a MEASURED, non-gating record (kit 2.1.0, ADR-043): zero gated findings, so
|
|
2775
|
+
it can never cap readiness nor block convergence, and stamped `advisory` so
|
|
2776
|
+
no surface counts it as an `ok` gate. Use it for a check the project has not
|
|
2777
|
+
declared as a gate -- `simplicity-check` in its default advisory mode above
|
|
2778
|
+
all. The alternative (persisting an advisory as `pass`) is the false clean
|
|
2779
|
+
this verdict exists to refuse: a reader cannot tell "the gate was green"
|
|
2780
|
+
from "there was no gate", and the second is what actually happened.
|
|
2781
|
+
not-run -> a steps event ONLY, never an iterations record: absence is not
|
|
2782
|
+
evidence — it neither reads as clean nor fakes a red (last state stands).
|
|
2783
|
+
|
|
2784
|
+
The engine cannot observe which mode a SEPARATE `simplicity-check` process ran in, so this
|
|
2785
|
+
is a named verdict rather than a refusal: refusing would only be enforceable on trust,
|
|
2786
|
+
while a third verdict is enforceable on the ledger. The caller declares the mode; every
|
|
2787
|
+
reader downstream then sees it as a fact instead of inferring it.
|
|
2756
2788
|
"""
|
|
2757
2789
|
# INV-ADVISORY-01 note (ADR-014): --kind is a CLOSED vocabulary (argparse choices), so
|
|
2758
2790
|
# an advisory-class dimension (e.g. "semantic") cannot be registered as a gate through
|
|
@@ -2774,12 +2806,27 @@ def cmd_log_gate(args):
|
|
|
2774
2806
|
f"(no evidence — last logged state stands, absence is never green)")
|
|
2775
2807
|
return
|
|
2776
2808
|
failing = args.verdict == "fail"
|
|
2809
|
+
advisory = args.verdict == "advisory"
|
|
2810
|
+
if advisory and args.kind not in ADVISORY_CAPABLE_KINDS:
|
|
2811
|
+
# ADR-043 widens --verdict for the checks that RUN advisory by default. A FACT gate
|
|
2812
|
+
# (deleted tests, a lowered threshold, a golden drift) recorded as advisory would be
|
|
2813
|
+
# a mandatory gate cleared by goodwill -- the exact thing this ledger exists to refuse.
|
|
2814
|
+
print(f"[qa_ledger] log-gate: --verdict advisory is not accepted for --kind {args.kind}: "
|
|
2815
|
+
f"only {', '.join(ADVISORY_CAPABLE_KINDS)} run in an advisory mode; a FACT gate "
|
|
2816
|
+
f"records pass, fail or not-run", file=sys.stderr)
|
|
2817
|
+
sys.exit(2)
|
|
2777
2818
|
rec = _append_gate_record(ledger, node, args.repo, tool, args.iteration,
|
|
2778
|
-
failing, args.count, args.note)
|
|
2819
|
+
failing, args.count, args.note, advisory=advisory)
|
|
2779
2820
|
_save(args.ledger, ledger)
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2821
|
+
if advisory:
|
|
2822
|
+
state, effect = "ADVISORY (measured, not gating)", (
|
|
2823
|
+
"reported everywhere as advisory, never as ok; caps nothing, blocks nothing")
|
|
2824
|
+
elif failing:
|
|
2825
|
+
state, effect = (f"FAIL (BLOCKER x{rec['gated_reported']})",
|
|
2826
|
+
"caps readiness <=65 and blocks convergence")
|
|
2827
|
+
else:
|
|
2828
|
+
state, effect = "PASS (clean)", "clears the gate for convergence"
|
|
2829
|
+
print(f"[qa_ledger] {args.repo}/{tool}: {state} logged — {effect}")
|
|
2783
2830
|
|
|
2784
2831
|
|
|
2785
2832
|
def cmd_flag_blocker(args):
|
|
@@ -8399,17 +8446,21 @@ def cmd_dashboard(args):
|
|
|
8399
8446
|
subscores = [{"k": "coverage",
|
|
8400
8447
|
"val": round(covp) if isinstance(covp, (int, float)) else None,
|
|
8401
8448
|
"bd": (f"{round(covp)}%" if isinstance(covp, (int, float)) else None)}]
|
|
8402
|
-
gate_block, gate_note = {}, {}
|
|
8449
|
+
gate_block, gate_note, gate_adv = {}, {}, {}
|
|
8403
8450
|
for g in rd.get("gates", []):
|
|
8404
8451
|
kind = (g.get("tool") or "").replace("gate:", "")
|
|
8405
8452
|
key = "golden" if kind.startswith("golden") else kind
|
|
8406
8453
|
gate_block[key] = gate_block.get(key, False) or bool(g.get("blocking"))
|
|
8454
|
+
gate_adv[key] = gate_adv.get(key, False) or bool(g.get("advisory"))
|
|
8407
8455
|
if g.get("note") and key not in gate_note:
|
|
8408
8456
|
gate_note[key] = g.get("note")
|
|
8409
8457
|
for key in ("simplicity", "waste", "golden"):
|
|
8410
8458
|
if key in gate_block:
|
|
8459
|
+
# ADR-043: a non-blocking ADVISORY is not "OK" — OK means a declared gate ran clean.
|
|
8460
|
+
_bd = ("FAIL" if gate_block[key]
|
|
8461
|
+
else "ADVISORY" if gate_adv.get(key) else "OK")
|
|
8411
8462
|
subscores.append({"k": key, "val": None,
|
|
8412
|
-
"bd": gate_note.get(key) or
|
|
8463
|
+
"bd": gate_note.get(key) or _bd})
|
|
8413
8464
|
|
|
8414
8465
|
# loops: iters + estado por repo (escalated > converged > active). max sin fuente.
|
|
8415
8466
|
# El estado se deriva ENTERO con _derive_phase (kit 1.48.1) — la MISMA funcion que
|
|
@@ -9033,6 +9084,10 @@ def _top_events(ledger, limit=TOP_EVENTS_TAIL):
|
|
|
9033
9084
|
gated = it.get("gated_reported")
|
|
9034
9085
|
if kind == "gate-not-run":
|
|
9035
9086
|
tail = "not run — nobody measured it"
|
|
9087
|
+
elif kind == "static-gate" and it.get("advisory"):
|
|
9088
|
+
# ADR-043: measured but not gating. `info` (never green, never red) is the
|
|
9089
|
+
# honest level -- rendering it `pass`/`clean` is the false clean again.
|
|
9090
|
+
level, tail = "info", "advisory — measured, not gating"
|
|
9036
9091
|
elif kind == "static-gate" and isinstance(gated, int):
|
|
9037
9092
|
level = "fail" if gated >= 1 else "pass"
|
|
9038
9093
|
tail = "%d gated finding(s)" % gated if gated else "clean"
|
|
@@ -9957,13 +10012,21 @@ def cmd_readiness(args):
|
|
|
9957
10012
|
gate_roll = out["gates"]
|
|
9958
10013
|
if gate_roll:
|
|
9959
10014
|
blocking = [g for g in gate_roll if g["blocking"]]
|
|
9960
|
-
|
|
10015
|
+
# ADR-043: an advisory NEVER joins the ok count. "3 ok" must mean three gates ran and
|
|
10016
|
+
# came back clean; folding a check the project never declared as a gate into that number
|
|
10017
|
+
# is the false clean the advisory verdict exists to refuse. The segment is conditional,
|
|
10018
|
+
# so a ledger with no advisory record prints exactly what it printed before.
|
|
10019
|
+
advisory = [g for g in gate_roll if g.get("advisory") and not g["blocking"]]
|
|
10020
|
+
n_ok = len(gate_roll) - len(blocking) - len(advisory)
|
|
10021
|
+
adv_str = (f" · {len(advisory)} advisory ("
|
|
10022
|
+
+ ", ".join(f"{g['repo']}/{g['tool']}" for g in advisory) + ")"
|
|
10023
|
+
if advisory else "")
|
|
9961
10024
|
hint = "" if args.verbose else " (readiness --verbose for the detail)"
|
|
9962
10025
|
if blocking:
|
|
9963
10026
|
names = ", ".join(f"{g['repo']}/{g['tool']}" for g in blocking)
|
|
9964
|
-
print(f"--- gates: {n_ok} ok · {len(blocking)} blocking ({names}){hint}")
|
|
10027
|
+
print(f"--- gates: {n_ok} ok{adv_str} · {len(blocking)} blocking ({names}){hint}")
|
|
9965
10028
|
else:
|
|
9966
|
-
print(f"--- gates: {n_ok} ok, none blocking{hint}")
|
|
10029
|
+
print(f"--- gates: {n_ok} ok{adv_str}, none blocking{hint}")
|
|
9967
10030
|
if not args.verbose:
|
|
9968
10031
|
return
|
|
9969
10032
|
print("--- dimensions (weight | raw | contribution) ---")
|
|
@@ -10023,6 +10086,14 @@ DEFAULT_COVERAGE_TOLERANCE = 5.0 # pct points the rebuilt coverage may drop
|
|
|
10023
10086
|
# abstraction is INTENTIONALLY not weighted: the "new types" regex is a prose/AST proxy
|
|
10024
10087
|
# that false-positives on Java records/DTOs, so it must not gate the band. It stays as an
|
|
10025
10088
|
# advisory metric + flag only (distilled: hard caps gate, guessy proxies advise).
|
|
10089
|
+
#
|
|
10090
|
+
# ADVISORY BY DEFAULT (kit 2.1.0, ADR-043). Every budget below is the KIT'S OPINION, not the
|
|
10091
|
+
# project's requirement, and an opinion that exits 1 is a gate nobody declared. Until a project
|
|
10092
|
+
# declares at least one numeric budget AND `defaults.simplicity.gate: true`, the verdict is
|
|
10093
|
+
# reported and the exit code is 0. This is NOT INV-ADVISORY-01 (that invariant quarantines
|
|
10094
|
+
# LLM-class JUDGMENT; these proxies are deterministic and may gate the moment a human says so)
|
|
10095
|
+
# -- it is the provenance rule of 1.17.0 applied to an exit code: a default is an opinion, and
|
|
10096
|
+
# only a declaration is a requirement.
|
|
10026
10097
|
SIMPLICITY_WEIGHTS = {
|
|
10027
10098
|
"diff_size": 35, "nesting": 30, "net_growth": 20, "fan_out": 8, "blob": 7,
|
|
10028
10099
|
}
|
|
@@ -10037,6 +10108,17 @@ SIMPLICITY_DEFAULTS = {
|
|
|
10037
10108
|
"max_abstraction_density": 3.0, # new *types* per 100 added LOC
|
|
10038
10109
|
"indent_width": 4,
|
|
10039
10110
|
}
|
|
10111
|
+
# Keys under defaults.simplicity that are NOT budgets, so declaring one never satisfies the
|
|
10112
|
+
# "a gate needs a budget" rule: `indent_width` is a PARSING parameter and `gate` is the switch
|
|
10113
|
+
# itself. `gate: true` with nothing but these declared is a refusal, not a gate (ADR-043).
|
|
10114
|
+
_SIMPLICITY_NON_BUDGET = ("indent_width", "gate")
|
|
10115
|
+
# What `max_nesting` actually measures, said once and reused by every surface that prints it.
|
|
10116
|
+
# It is INDENTATION DEPTH over added lines, not AST nesting: a wrapped call argument, JSX, a
|
|
10117
|
+
# multi-line Java string or any deep continuation raises it without any control flow existing.
|
|
10118
|
+
# The kit does NOT make it language-aware (that needs a parser per stack, which this stdlib
|
|
10119
|
+
# engine will not have) -- it names the proxy instead, so a reader can discount it.
|
|
10120
|
+
_NESTING_PROXY_NOTE = ("indentation depth over added lines, NOT AST nesting -- continuation "
|
|
10121
|
+
"lines, JSX and multi-line literals inflate it")
|
|
10040
10122
|
# code files only — docs, config, resources and generated trees are noise for a
|
|
10041
10123
|
# code-simplicity gate. Broader than SOURCE_EXT (which is repo-typed for rebuild).
|
|
10042
10124
|
_SIMPLICITY_CODE_EXT = {
|
|
@@ -10449,8 +10531,9 @@ def _simplicity_score(m, b):
|
|
|
10449
10531
|
def _simplicity_flags(m, b):
|
|
10450
10532
|
f = []
|
|
10451
10533
|
if m["max_nesting"] > b["max_nesting_depth"]:
|
|
10452
|
-
f.append(f"
|
|
10453
|
-
f"aplanar: guard clauses / extraer función
|
|
10534
|
+
f.append(f"max_nesting (indentation proxy) {m['max_nesting']} > "
|
|
10535
|
+
f"{b['max_nesting_depth']} — aplanar: guard clauses / extraer función "
|
|
10536
|
+
f"(CWE-1124). Proxy: {_NESTING_PROXY_NOTE}")
|
|
10454
10537
|
if m["new_abstractions"] > b["max_new_abstractions"]:
|
|
10455
10538
|
f.append(f"{m['new_abstractions']} tipos/capas nuevos > "
|
|
10456
10539
|
f"{b['max_new_abstractions']} — ¿todos pedidos? "
|
|
@@ -10476,10 +10559,12 @@ def _simplicity_flags(m, b):
|
|
|
10476
10559
|
def cmd_simplicity_check(args):
|
|
10477
10560
|
b = dict(SIMPLICITY_DEFAULTS)
|
|
10478
10561
|
declared = set() # presupuestos declarados por el humano (config o CLI)
|
|
10562
|
+
gate = False # ADR-043: solo lo enciende una DECLARACION, nunca un default
|
|
10479
10563
|
if args.config and os.path.exists(args.config):
|
|
10480
10564
|
cfg = _load(args.config).get("defaults", {}).get("simplicity", {})
|
|
10481
10565
|
b.update({k: cfg[k] for k in b if k in cfg})
|
|
10482
|
-
declared |= {k for k in b if k in cfg and k
|
|
10566
|
+
declared |= {k for k in b if k in cfg and k not in _SIMPLICITY_NON_BUDGET}
|
|
10567
|
+
gate = bool(cfg.get("gate"))
|
|
10483
10568
|
for k in ("max_lines_added", "max_net_lines", "max_files_changed",
|
|
10484
10569
|
"max_nesting_depth", "max_hunk_added", "max_new_abstractions",
|
|
10485
10570
|
"indent_width"):
|
|
@@ -10491,34 +10576,55 @@ def cmd_simplicity_check(args):
|
|
|
10491
10576
|
if args.max_abstraction_density is not None:
|
|
10492
10577
|
b["max_abstraction_density"] = args.max_abstraction_density
|
|
10493
10578
|
declared.add("max_abstraction_density")
|
|
10579
|
+
if getattr(args, "gate", False):
|
|
10580
|
+
gate = True
|
|
10581
|
+
# A gate with no budget is not a gate: it is the kit's opinion wearing an exit code, which
|
|
10582
|
+
# is exactly the defect ADR-043 exists to remove. Refuse BEFORE reading the diff -- a
|
|
10583
|
+
# misconfigured gate must not produce a score anyone could quote.
|
|
10584
|
+
if gate and not declared:
|
|
10585
|
+
print("[qa_ledger] invalid config: defaults.simplicity.gate is true (or --gate was "
|
|
10586
|
+
"passed) but no simplicity budget is declared — a gate with no budget is not a "
|
|
10587
|
+
"gate, only the kit's opinion with an exit code. Declare at least one of "
|
|
10588
|
+
"max_lines_added, max_net_lines, max_files_changed, max_nesting_depth, "
|
|
10589
|
+
"max_hunk_added, max_new_abstractions, max_abstraction_density in "
|
|
10590
|
+
"defaults.simplicity (or pass the matching --max-... flag), or set gate to false.",
|
|
10591
|
+
file=sys.stderr)
|
|
10592
|
+
sys.exit(2)
|
|
10593
|
+
mode = "gate" if gate else "advisory"
|
|
10494
10594
|
|
|
10495
10595
|
m = _simplicity_metrics(_read_diff(args), b["indent_width"])
|
|
10496
10596
|
score, dims = _simplicity_score(m, b)
|
|
10497
10597
|
verdict = _simplicity_band(score)
|
|
10498
10598
|
flags = _simplicity_flags(m, b)
|
|
10599
|
+
exit_code = 1 if (verdict == "OVERBUILT" and gate) else 0
|
|
10499
10600
|
|
|
10500
|
-
out = {"score": score, "verdict": verdict, "
|
|
10601
|
+
out = {"score": score, "verdict": verdict, "mode": mode, "gate": gate,
|
|
10602
|
+
"weights": SIMPLICITY_WEIGHTS,
|
|
10501
10603
|
"dimensions": {k: round(v, 3) for k, v in dims.items()},
|
|
10502
|
-
"metrics": m, "
|
|
10604
|
+
"metrics": m, "metrics_notes": {"max_nesting": _NESTING_PROXY_NOTE},
|
|
10605
|
+
"budgets": b, "budgets_declared": sorted(declared),
|
|
10503
10606
|
"flags": flags}
|
|
10504
10607
|
if args.json:
|
|
10505
10608
|
print(json.dumps(out, indent=2, ensure_ascii=False))
|
|
10506
|
-
sys.exit(
|
|
10609
|
+
sys.exit(exit_code)
|
|
10507
10610
|
|
|
10508
|
-
|
|
10611
|
+
mode_str = ("declared gate" if gate else
|
|
10612
|
+
"advisory (declare budgets + defaults.simplicity.gate to make it block)")
|
|
10613
|
+
print(f"SIMPLICITY: {score}/100 — {verdict} ({mode_str})")
|
|
10509
10614
|
print("--- metrics (value / budget · * = declared by the human) ---")
|
|
10510
10615
|
rows = [
|
|
10511
10616
|
("lines_added", m["lines_added"], b["max_lines_added"], "max_lines_added"),
|
|
10512
10617
|
("net_lines", m["net_lines"], b["max_net_lines"], "max_net_lines"),
|
|
10513
10618
|
("files_changed", m["files_changed"], b["max_files_changed"], "max_files_changed"),
|
|
10514
|
-
("max_nesting", m["max_nesting"], b["max_nesting_depth"], "max_nesting_depth"),
|
|
10619
|
+
("max_nesting (indentation proxy)", m["max_nesting"], b["max_nesting_depth"], "max_nesting_depth"),
|
|
10515
10620
|
("new_abstractions", m["new_abstractions"], b["max_new_abstractions"], "max_new_abstractions"),
|
|
10516
10621
|
("abstraction/100", m["abstraction_density"], b["max_abstraction_density"], "max_abstraction_density"),
|
|
10517
10622
|
("max_hunk_added", m["max_hunk_added"], b["max_hunk_added"], "max_hunk_added"),
|
|
10518
10623
|
]
|
|
10519
10624
|
for name, val, bud, key in rows:
|
|
10520
10625
|
mark = "*" if key in declared else ""
|
|
10521
|
-
print(f" {name:
|
|
10626
|
+
print(f" {name:31s} {str(val):>7s} / {bud}{mark}")
|
|
10627
|
+
print(f" (max_nesting is a PROXY: {_NESTING_PROXY_NOTE})")
|
|
10522
10628
|
if not declared:
|
|
10523
10629
|
print(" (every budget is a kit default — an opinion, not a "
|
|
10524
10630
|
"requirement: declare yours in config.defaults.simplicity)")
|
|
@@ -10535,7 +10641,11 @@ def cmd_simplicity_check(args):
|
|
|
10535
10641
|
print(f" ! {fl}")
|
|
10536
10642
|
else:
|
|
10537
10643
|
print(" within budget — nothing to cut")
|
|
10538
|
-
|
|
10644
|
+
if verdict == "OVERBUILT" and not gate:
|
|
10645
|
+
print("--- advisory: OVERBUILT is REPORTED, not enforced (exit 0). Cut what is cheap, "
|
|
10646
|
+
"say so in the PR body, and do not let it block the loop. To make it block, "
|
|
10647
|
+
"declare your budgets AND defaults.simplicity.gate: true ---")
|
|
10648
|
+
sys.exit(exit_code)
|
|
10539
10649
|
|
|
10540
10650
|
|
|
10541
10651
|
# --------------------------------------------------------------------------- #
|
|
@@ -12876,7 +12986,11 @@ def build_parser():
|
|
|
12876
12986
|
plg.add_argument("--kind", required=True,
|
|
12877
12987
|
choices=["golden-diff", "gate-check", "pit-check", "simplicity",
|
|
12878
12988
|
"regression", "rubric", "waste"])
|
|
12879
|
-
plg.add_argument("--verdict", required=True,
|
|
12989
|
+
plg.add_argument("--verdict", required=True,
|
|
12990
|
+
choices=["pass", "fail", "advisory", "not-run"],
|
|
12991
|
+
help="advisory (ADR-043) records a measured, non-gating run: it never "
|
|
12992
|
+
"caps readiness, never blocks convergence, and never reads as ok; "
|
|
12993
|
+
"accepted only for --kind simplicity|waste, a FACT gate refuses it")
|
|
12880
12994
|
plg.add_argument("--count", type=int, default=1,
|
|
12881
12995
|
help="failing finding count (fail only; default 1)")
|
|
12882
12996
|
plg.add_argument("--note", default=None)
|
|
@@ -13057,7 +13171,9 @@ def build_parser():
|
|
|
13057
13171
|
|
|
13058
13172
|
ps2 = sub.add_parser(
|
|
13059
13173
|
"simplicity-check",
|
|
13060
|
-
help="Reduce gate: score diff minimality/complexity over a unified diff"
|
|
13174
|
+
help="Reduce gate: score diff minimality/complexity over a unified diff. Advisory "
|
|
13175
|
+
"by default; gates only with --gate or defaults.simplicity.gate AND at least "
|
|
13176
|
+
"one declared budget")
|
|
13061
13177
|
ps2.add_argument("--diff", help="path to a unified diff (else --from-git or stdin)")
|
|
13062
13178
|
ps2.add_argument("--from-git", action="store_true",
|
|
13063
13179
|
help="run `git diff --unified=0 <base>` for the diff")
|
|
@@ -13073,6 +13189,10 @@ def build_parser():
|
|
|
13073
13189
|
ps2.add_argument("--max-abstraction-density", dest="max_abstraction_density",
|
|
13074
13190
|
type=float, default=None)
|
|
13075
13191
|
ps2.add_argument("--indent-width", dest="indent_width", type=int)
|
|
13192
|
+
ps2.add_argument("--gate", action="store_true",
|
|
13193
|
+
help="make an OVERBUILT verdict exit 1 (same switch as "
|
|
13194
|
+
"defaults.simplicity.gate). Requires at least one declared budget: "
|
|
13195
|
+
"without one the run REFUSES with exit 2 (ADR-043)")
|
|
13076
13196
|
ps2.add_argument("--json", action="store_true")
|
|
13077
13197
|
ps2.set_defaults(func=cmd_simplicity_check)
|
|
13078
13198
|
|
|
@@ -37,9 +37,11 @@ enforcing the record is the engine's job. It is never resolved by "working aroun
|
|
|
37
37
|
|
|
38
38
|
## Simplicity — "Reduce" (non-negotiable)
|
|
39
39
|
|
|
40
|
-
> Maeda's law 1 and Karpathy's "Simplicity First", made a deterministic
|
|
40
|
+
> Maeda's law 1 and Karpathy's "Simplicity First", made a deterministic check.
|
|
41
41
|
> It is not CC by AST: they are measurable *proxies* over the diff. Measured by
|
|
42
|
-
> `qa_ledger.py simplicity-check
|
|
42
|
+
> `qa_ledger.py simplicity-check`, **advisory by default** (exit 0); with your own budgets
|
|
43
|
+
> declared **and** `defaults.simplicity.gate: true`, an **OVERBUILT** verdict is a
|
|
44
|
+
> **BLOCKER** finding.
|
|
43
45
|
|
|
44
46
|
- [ ] Minimal code that solves what was asked — no unrequested features, layers or "flexibility" <!-- YAGNI / speculative generality -->
|
|
45
47
|
- [ ] No speculative abstractions — every new type/layer is justified against the SPEC <!-- YAGNI -->
|
|
@@ -133,7 +135,12 @@ enforcing the record is the engine's job. It is never resolved by "working aroun
|
|
|
133
135
|
Detecting it is the agent/human's obligation; once recorded, enforcement is the engine's.
|
|
134
136
|
- The **Simplicity** invariant is measured without human judgment: `qa_ledger.py simplicity-check`
|
|
135
137
|
scores the diff (minimality, nesting, abstraction) and returns `SIMPLE / ACCEPTABLE /
|
|
136
|
-
OVERBUILT`. **
|
|
138
|
+
OVERBUILT`. **Advisory by default** (exit 0, kit 2.1.0): the shipped budgets are the kit's
|
|
139
|
+
opinion, and an opinion never blocks a loop. With your own budgets declared in
|
|
140
|
+
`defaults.simplicity` **and** `defaults.simplicity.gate: true` (or `--gate`), **OVERBUILT** =
|
|
141
|
+
BLOCKER (exit 1): it is trimmed, not converged. `gate: true` without a declared budget is
|
|
142
|
+
refused (exit 2). Note that `max_nesting` is an INDENTATION-DEPTH proxy, so JSX and
|
|
143
|
+
multi-line literals inflate it — read it as such before trimming.
|
|
137
144
|
- The **Reuse (REUSE-FIRST)** invariant is measured by `qa_ledger.py waste-check`: Type-1/2 clones
|
|
138
145
|
of the diff vs the repo (`dup_vs_repo` is the dominant signal). **Advisory by default** (advises
|
|
139
146
|
with `file:line` to reuse, exit 0); with `defaults.waste.gate: true` or `--gate` a
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_comment": "COMPREHENSIVE REFERENCE, not a project config. Every knob the engine and the skills understand, at the kit's own value, so a human can read what can be declared. `uscha init` does NOT copy this file: it GENERATES a minimal project config, because a copied default is an explicit declaration and an explicit declaration outranks the preset named by defaults.risk_profile (ADR-001, as amended). Copy a block from here into your project only when you mean to override the engine default or the preset. NOTE: no version string may be written into this comment -- the release script requires exactly one occurrence of the version in this file (I3), and a second one refuses the next release.",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"project": null,
|
|
5
5
|
"defaults": {
|
|
6
6
|
"coverage_threshold": 60,
|
|
@@ -73,7 +73,8 @@
|
|
|
73
73
|
"max_hunk_added": 120,
|
|
74
74
|
"max_new_abstractions": 8,
|
|
75
75
|
"max_abstraction_density": 3.0,
|
|
76
|
-
"indent_width": 4
|
|
76
|
+
"indent_width": 4,
|
|
77
|
+
"gate": false
|
|
77
78
|
},
|
|
78
79
|
"waste": {
|
|
79
80
|
"window_size": 5,
|