@miller-tech/uap 1.221.0 → 2.0.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 +19 -9
- package/config/exl3-profiles/qwen38-27b-exl3.env +68 -0
- package/config/llama-profiles/qwen38-27b-dflash2.env +6 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/benchmarks/paired/adapter.d.ts +15 -0
- package/dist/benchmarks/paired/adapter.d.ts.map +1 -1
- package/dist/benchmarks/paired/adapter.js +22 -1
- package/dist/benchmarks/paired/adapter.js.map +1 -1
- package/dist/bin/cli.js +20 -24
- package/dist/bin/cli.js.map +1 -1
- package/dist/cli/deliver.d.ts +34 -0
- package/dist/cli/deliver.d.ts.map +1 -1
- package/dist/cli/deliver.js +147 -20
- package/dist/cli/deliver.js.map +1 -1
- package/dist/cli/guided-setup.d.ts.map +1 -1
- package/dist/cli/guided-setup.js +15 -0
- package/dist/cli/guided-setup.js.map +1 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +3 -0
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/plan.d.ts.map +1 -1
- package/dist/cli/plan.js +8 -2
- package/dist/cli/plan.js.map +1 -1
- package/dist/cli/policy.d.ts.map +1 -1
- package/dist/cli/policy.js +104 -2
- package/dist/cli/policy.js.map +1 -1
- package/dist/cli/quality.d.ts +11 -0
- package/dist/cli/quality.d.ts.map +1 -0
- package/dist/cli/quality.js +213 -0
- package/dist/cli/quality.js.map +1 -0
- package/dist/cli/wizard-config.d.ts.map +1 -1
- package/dist/cli/wizard-config.js +19 -9
- package/dist/cli/wizard-config.js.map +1 -1
- package/dist/cli/worktree.d.ts.map +1 -1
- package/dist/cli/worktree.js +8 -1
- package/dist/cli/worktree.js.map +1 -1
- package/dist/dashboard/savings.d.ts +1 -4
- package/dist/dashboard/savings.d.ts.map +1 -1
- package/dist/dashboard/savings.js +2 -52
- package/dist/dashboard/savings.js.map +1 -1
- package/dist/delivery/acceptance-judge.d.ts +10 -0
- package/dist/delivery/acceptance-judge.d.ts.map +1 -1
- package/dist/delivery/acceptance-judge.js +45 -3
- package/dist/delivery/acceptance-judge.js.map +1 -1
- package/dist/delivery/agentic-executor.d.ts +12 -1
- package/dist/delivery/agentic-executor.d.ts.map +1 -1
- package/dist/delivery/agentic-executor.js +25 -5
- package/dist/delivery/agentic-executor.js.map +1 -1
- package/dist/delivery/bash-sweep.d.ts +12 -0
- package/dist/delivery/bash-sweep.d.ts.map +1 -1
- package/dist/delivery/bash-sweep.js +8 -1
- package/dist/delivery/bash-sweep.js.map +1 -1
- package/dist/delivery/convergence-loop.d.ts.map +1 -1
- package/dist/delivery/convergence-loop.js +6 -4
- package/dist/delivery/convergence-loop.js.map +1 -1
- package/dist/delivery/execution-gate.d.ts +1 -1
- package/dist/delivery/execution-gate.d.ts.map +1 -1
- package/dist/delivery/execution-gate.js +248 -5
- package/dist/delivery/execution-gate.js.map +1 -1
- package/dist/delivery/snapshot.d.ts +16 -0
- package/dist/delivery/snapshot.d.ts.map +1 -1
- package/dist/delivery/snapshot.js +113 -2
- package/dist/delivery/snapshot.js.map +1 -1
- package/dist/delivery/user-validation.d.ts.map +1 -1
- package/dist/delivery/user-validation.js +55 -10
- package/dist/delivery/user-validation.js.map +1 -1
- package/dist/delivery/verifier-ladder.d.ts +24 -0
- package/dist/delivery/verifier-ladder.d.ts.map +1 -1
- package/dist/delivery/verifier-ladder.js +119 -2
- package/dist/delivery/verifier-ladder.js.map +1 -1
- package/dist/delivery/visual-gate.d.ts +24 -0
- package/dist/delivery/visual-gate.d.ts.map +1 -1
- package/dist/delivery/visual-gate.js +70 -9
- package/dist/delivery/visual-gate.js.map +1 -1
- package/dist/generators/claude-md.js +17 -4
- package/dist/generators/claude-md.js.map +1 -1
- package/dist/policies/liveness.d.ts +105 -0
- package/dist/policies/liveness.d.ts.map +1 -0
- package/dist/policies/liveness.js +251 -0
- package/dist/policies/liveness.js.map +1 -0
- package/dist/policies/policy-memory.d.ts.map +1 -1
- package/dist/policies/policy-memory.js +3 -4
- package/dist/policies/policy-memory.js.map +1 -1
- package/dist/quality/baseline.d.ts +29 -0
- package/dist/quality/baseline.d.ts.map +1 -0
- package/dist/quality/baseline.js +79 -0
- package/dist/quality/baseline.js.map +1 -0
- package/dist/quality/complexity.d.ts +38 -0
- package/dist/quality/complexity.d.ts.map +1 -0
- package/dist/quality/complexity.js +239 -0
- package/dist/quality/complexity.js.map +1 -0
- package/dist/quality/config.d.ts +40 -0
- package/dist/quality/config.d.ts.map +1 -0
- package/dist/quality/config.js +77 -0
- package/dist/quality/config.js.map +1 -0
- package/dist/quality/crap.d.ts +11 -0
- package/dist/quality/crap.d.ts.map +1 -0
- package/dist/quality/crap.js +54 -0
- package/dist/quality/crap.js.map +1 -0
- package/dist/quality/languages.d.ts +25 -0
- package/dist/quality/languages.d.ts.map +1 -0
- package/dist/quality/languages.js +150 -0
- package/dist/quality/languages.js.map +1 -0
- package/dist/quality/report.d.ts +38 -0
- package/dist/quality/report.d.ts.map +1 -0
- package/dist/quality/report.js +251 -0
- package/dist/quality/report.js.map +1 -0
- package/dist/quality/scanner.d.ts +32 -0
- package/dist/quality/scanner.d.ts.map +1 -0
- package/dist/quality/scanner.js +127 -0
- package/dist/quality/scanner.js.map +1 -0
- package/dist/quality/tools.d.ts +28 -0
- package/dist/quality/tools.d.ts.map +1 -0
- package/dist/quality/tools.js +189 -0
- package/dist/quality/tools.js.map +1 -0
- package/dist/types/config.d.ts +148 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +35 -0
- package/dist/types/config.js.map +1 -1
- package/docs/ATTRIBUTION.md +0 -1
- package/docs/INDEX.md +38 -4
- package/docs/analysis/MARKET_POSITION.md +179 -0
- package/docs/architecture/OVERVIEW.md +82 -18
- package/docs/architecture/PROTOCOL.md +1 -1
- package/docs/benchmarks/PAIRED_FINDINGS.md +106 -0
- package/docs/benchmarks/PAIRED_HARNESS.md +16 -0
- package/docs/benchmarks/README.md +59 -0
- package/docs/design/UAP_REACTOR.md +0 -1
- package/docs/getting-started/INSTALLATION.md +1 -1
- package/docs/guides/AUTOMATIC.md +0 -14
- package/docs/guides/AUTOMATIC_FEATURES.md +1 -9
- package/docs/guides/COORDINATION.md +1 -1
- package/docs/guides/DELIVERY_PIPELINE.md +5 -0
- package/docs/guides/DEPLOY_BATCHING.md +1 -1
- package/docs/guides/DROIDS_AND_SKILLS.md +1 -1
- package/docs/guides/LOCAL_MODELS.md +6 -1
- package/docs/guides/MCP_ROUTER.md +1 -1
- package/docs/guides/MEMORY.md +1 -1
- package/docs/guides/MULTI_MODEL.md +1 -1
- package/docs/guides/ORCHESTRATOR.md +1 -1
- package/docs/guides/POLICIES.md +19 -3
- package/docs/guides/QWEN38_EXL3.md +114 -0
- package/docs/guides/WORKTREE_WORKFLOW.md +45 -1
- package/docs/integrations/MCP_ROUTER.md +1 -4
- package/docs/plans/deliver-hardening-plan-2026-07-13.md +173 -0
- package/docs/plans/deliver-hardening-review-response-2026-07-13.md +180 -0
- package/docs/plans/k3-uap-uplift-validation.md +216 -0
- package/docs/plans/qwen38-uap-retest-plan.md +196 -0
- package/docs/reference/API.md +1 -1
- package/docs/reference/CLI.md +49 -14
- package/docs/reference/COMPARISON.md +248 -0
- package/docs/reference/CONFIGURATION.md +1 -3
- package/docs/reference/DATABASE_SCHEMA.md +1 -1
- package/docs/reference/FEATURES.md +3 -3
- package/docs/reference/PATTERNS.md +1 -1
- package/docs/reference/PLATFORMS.md +1 -1
- package/package.json +2 -2
- package/scripts/maintenance/update-skills.py +0 -22
- package/scripts/maintenance/verify-compliance.sh +3 -3
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/src/policies/enforcers/__pycache__/quality_metrics_gate.cpython-312.pyc +0 -0
- package/src/policies/enforcers/adr_guard.py +0 -0
- package/src/policies/enforcers/bearer_lockdown.py +0 -0
- package/src/policies/enforcers/branch_freshness.py +0 -0
- package/src/policies/enforcers/design_token_gate.py +0 -0
- package/src/policies/enforcers/enforcement_self_protect.py +70 -0
- package/src/policies/enforcers/expert_review_required.py +22 -0
- package/src/policies/enforcers/iac_plan_destruction_check.py +0 -0
- package/src/policies/enforcers/quality_metrics_gate.py +447 -0
- package/src/policies/enforcers/ship_loop_gate.py +0 -0
- package/src/policies/enforcers/validate_plan_on_change.py +0 -0
- package/src/policies/enforcers/visual_verification.py +0 -0
- package/src/policies/enforcers/workdir_scope.py +0 -0
- package/src/policies/schemas/policies/expert-review-required.md +9 -1
- package/src/policies/schemas/policies/quality-metrics-gate.md +74 -0
- package/templates/CLAUDE.template.md +1 -1
- package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
- package/templates/hooks/pre-tool-use-bash.sh +50 -3
- package/templates/hooks/pre-tool-use-edit-write.sh +31 -0
- package/templates/hooks/session-start.sh +15 -0
- package/templates/hooks/uap-policy-gate.sh +187 -5
- package/templates/stryker.config.json +14 -0
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
- package/tools/agents/scripts/anthropic_proxy.py +254 -28
- package/tools/agents/tests/run_acceptance.py +0 -0
- package/tools/agents/tests/test_client_disconnect.py +0 -0
- package/tools/agents/tests/test_cycle_break_exploration.py +0 -0
- package/tools/agents/tests/test_deferral_break.py +0 -0
- package/tools/agents/tests/test_delivery_enforcement_all_langs.py +0 -0
- package/tools/agents/tests/test_delivery_enforcement_exemptions.py +0 -0
- package/tools/agents/tests/test_delivery_enforcement_filepath.py +0 -0
- package/tools/agents/tests/test_delivery_enforcement_web_and_bash.py +0 -0
- package/tools/agents/tests/test_delivery_enforcement_worktree.py +0 -0
- package/tools/agents/tests/test_delivery_enforcement_write_complexity.py +0 -0
- package/tools/agents/tests/test_disconnect_watcher.py +0 -0
- package/tools/agents/tests/test_doubling_break.py +0 -0
- package/tools/agents/tests/test_empty_maxtokens_recovery.py +0 -0
- package/tools/agents/tests/test_empty_tool_loop_break.py +0 -0
- package/tools/agents/tests/test_enforcement_self_protect.py +90 -0
- package/tools/agents/tests/test_enforcer_escape_hatches.py +0 -0
- package/tools/agents/tests/test_error_loop_break.py +0 -0
- package/tools/agents/tests/test_expert_review_pr_scope.py +13 -5
- package/tools/agents/tests/test_fastpath_config.py +0 -0
- package/tools/agents/tests/test_finalize_suppression.py +0 -0
- package/tools/agents/tests/test_gate_evidence.py +0 -0
- package/tools/agents/tests/test_gate_failclosed_indirection.py +0 -0
- package/tools/agents/tests/test_gate_integrity.py +0 -0
- package/tools/agents/tests/test_live_deliver_kill.py +0 -0
- package/tools/agents/tests/test_malformed_unclosed_think.py +0 -0
- package/tools/agents/tests/test_mandate_beats_recon.py +0 -0
- package/tools/agents/tests/test_mandate_deliver.py +0 -0
- package/tools/agents/tests/test_non_stream_resp_log.py +0 -0
- package/tools/agents/tests/test_output_token_ceilings.py +0 -0
- package/tools/agents/tests/test_overflow_truncate_count_tokens.py +0 -0
- package/tools/agents/tests/test_path_containment.py +0 -0
- package/tools/agents/tests/test_path_normalizer_hardened.py +0 -0
- package/tools/agents/tests/test_repeat_call_guard.py +0 -0
- package/tools/agents/tests/test_repeat_guard.py +0 -0
- package/tools/agents/tests/test_session_admission.py +0 -0
- package/tools/agents/tests/test_stream_heartbeat.py +0 -0
- package/tools/agents/tests/test_stream_thinking_deltas.py +405 -0
- package/tools/agents/tests/test_stuck_break_hard.py +0 -0
- package/tools/agents/tests/test_stuck_break_reattach.py +0 -0
- package/tools/agents/tests/test_turn_count_breaker_periodic.py +0 -0
- package/tools/agents/tests/test_upstream_chokepoint.py +0 -0
- package/tools/agents/tests/test_upstream_model_aliases.py +0 -0
- package/tools/agents/tests/test_validate_plan_gate.py +0 -0
- package/tools/agents/tests/test_vision_passthrough.py +0 -0
- package/tools/agents/tests/test_workdir_scope_enforcer.py +0 -0
- package/dist/cli/rtk.d.ts +0 -34
- package/dist/cli/rtk.d.ts.map +0 -1
- package/dist/cli/rtk.js +0 -401
- package/dist/cli/rtk.js.map +0 -1
- package/docs/integrations/RTK.md +0 -113
- package/scripts/setup/install-rtk.sh +0 -184
- package/src/policies/enforcers/rtk_wrap.py +0 -205
- package/src/policies/schemas/policies/rtk-wrap.md +0 -50
- package/tools/agents/tests/test_rtk_wrap.py +0 -151
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""quality-metrics gate: edits must not introduce NEW or WORSENED metric debt.
|
|
3
|
+
|
|
4
|
+
A Write/Edit/MultiEdit to a source file whose post-edit content violates the
|
|
5
|
+
thresholds in `.uap/quality-metrics.json` is **blocked** — unless the violation
|
|
6
|
+
is already recorded in `.uap/quality-baseline.json` at an equal-or-worse value
|
|
7
|
+
(the ratchet: existing debt is frozen, new debt is refused).
|
|
8
|
+
|
|
9
|
+
Fast-path metrics computed here (stdlib-only mirror of src/quality/):
|
|
10
|
+
- LOC per file (maxLocPerFile, default 500)
|
|
11
|
+
- cyclomatic complexity (maxCyclomatic, default 22) — heuristic fallback
|
|
12
|
+
- cognitive complexity (maxCognitive, default 22) — heuristic fallback
|
|
13
|
+
- explicit any/unknown (maxAnyTypes, default 0) — TS/JS/Py
|
|
14
|
+
|
|
15
|
+
Slow metrics (coverage, CRAP, Halstead, duplication, dead code, mutation) need
|
|
16
|
+
external data and are enforced by `uap quality check` at commit/CI time, not
|
|
17
|
+
per edit.
|
|
18
|
+
|
|
19
|
+
The gate is INACTIVE (fails open) when `.uap/quality-metrics.json` is absent —
|
|
20
|
+
quality policing is opt-in per project via `uap quality init`. Escape hatch:
|
|
21
|
+
UAP_QUALITY_GATE_OFF=1. Mirrors src/quality/{scanner,complexity,baseline}.ts.
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import re
|
|
28
|
+
import sys
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
32
|
+
from _common import emit, parse_cli, repo_root, worktree_root # noqa: E402
|
|
33
|
+
|
|
34
|
+
PATH_WRITE_OPS = {"Write", "Edit", "MultiEdit", "write", "edit", "multiedit"}
|
|
35
|
+
|
|
36
|
+
EXT_LANG = {
|
|
37
|
+
".ts": "typescript", ".tsx": "typescript", ".mts": "typescript", ".cts": "typescript",
|
|
38
|
+
".js": "javascript", ".jsx": "javascript", ".mjs": "javascript", ".cjs": "javascript",
|
|
39
|
+
".py": "python", ".java": "java", ".cs": "csharp",
|
|
40
|
+
".cpp": "cpp", ".cc": "cpp", ".cxx": "cpp", ".hpp": "cpp", ".hxx": "cpp",
|
|
41
|
+
".c": "c", ".h": "c", ".rs": "rust", ".go": "go", ".rb": "ruby",
|
|
42
|
+
".php": "php", ".swift": "swift", ".kt": "kotlin", ".kts": "kotlin", ".scala": "scala",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
C_DECISION_WORDS = ("if", "for", "while", "case", "catch", "elif")
|
|
46
|
+
C_DECISION_OPS = ("&&", "||", "?")
|
|
47
|
+
PY_DECISION_WORDS = ("if", "elif", "for", "while", "except", "with", "assert")
|
|
48
|
+
PY_DECISION_OPS = ("and", "or")
|
|
49
|
+
|
|
50
|
+
# Boolean/ternary ops get a flat +1 cognitive (no nesting bump). Keep in
|
|
51
|
+
# lockstep with BOOL_OPS in src/quality/complexity.ts.
|
|
52
|
+
_BOOL_OPS = frozenset({"&&", "||", "?", "and", "or"})
|
|
53
|
+
|
|
54
|
+
RE_FUNC_KW = re.compile(r"\b(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s*\*?\s*([A-Za-z_$][\w$]*)\s*\(")
|
|
55
|
+
RE_NAMED_KW = re.compile(r"\b(?:def|fn|func|fun)\s+([A-Za-z_]\w*)\s*\(")
|
|
56
|
+
# Arrow functions assigned to consts — pervasive in modern TS; missing them
|
|
57
|
+
# dumps their bodies into the <module> score.
|
|
58
|
+
RE_ARROW = re.compile(
|
|
59
|
+
r"^\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::\s*[^=]+?)?="
|
|
60
|
+
r"\s*(?:async\s+)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>\s*\{"
|
|
61
|
+
)
|
|
62
|
+
RE_METHOD = re.compile(
|
|
63
|
+
r"^\s*(?:(?:public|private|protected|static|final|abstract|override|readonly|async|"
|
|
64
|
+
r"inline|virtual|constexpr|pub|unsafe|extern|internal|sealed|partial|suspend|open|"
|
|
65
|
+
r"export|default|get|set)\s+)*([A-Za-z_$][\w$]*)\s*\([^;{}]*\)\s*(?::\s*[^{;]+?)?\s*\{"
|
|
66
|
+
)
|
|
67
|
+
CONTROL_WORDS = {
|
|
68
|
+
"if", "for", "while", "switch", "catch", "return", "do", "else", "case",
|
|
69
|
+
"with", "synchronized", "lock", "using", "foreach", "when", "unless", "until",
|
|
70
|
+
}
|
|
71
|
+
RE_PY_DEF = re.compile(r"^(\s*)(?:async\s+)?def\s+([A-Za-z_]\w*)\s*\(")
|
|
72
|
+
|
|
73
|
+
ANY_PATTERNS = {
|
|
74
|
+
"typescript": re.compile(r":\s*any\b|:\s*unknown\b|\bas\s+any\b|<any>|<unknown>|Array<\s*any\s*>"),
|
|
75
|
+
"javascript": re.compile(r"/\*\s*@type\s*\{[^}]*\b(?:any|unknown)\b[^}]*\}\s*\*/"),
|
|
76
|
+
"python": re.compile(r":\s*Any\b|->\s*Any\b|cast\(\s*Any\b"),
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
TOKEN_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*|&&|\|\||[?{}]")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _strip_noise(content: str, comment_style: str) -> str:
|
|
83
|
+
"""Remove strings and comments (state machine, mirrors languages.ts)."""
|
|
84
|
+
out: list[str] = []
|
|
85
|
+
i, n = 0, len(content)
|
|
86
|
+
state = "code"
|
|
87
|
+
while i < n:
|
|
88
|
+
ch = content[i]
|
|
89
|
+
nxt = content[i + 1] if i + 1 < n else ""
|
|
90
|
+
if state == "code":
|
|
91
|
+
if ch == "/" and nxt == "/":
|
|
92
|
+
state = "line"; i += 2; continue
|
|
93
|
+
if ch == "/" and nxt == "*":
|
|
94
|
+
state = "block"; i += 2; continue
|
|
95
|
+
if comment_style == "hash" and ch == "#":
|
|
96
|
+
state = "line"; i += 1; continue
|
|
97
|
+
if ch in ("'", '"', "`"):
|
|
98
|
+
state = ch; out.append(" "); i += 1; continue
|
|
99
|
+
out.append(ch); i += 1; continue
|
|
100
|
+
if state == "line":
|
|
101
|
+
if ch == "\n":
|
|
102
|
+
state = "code"; out.append("\n")
|
|
103
|
+
i += 1; continue
|
|
104
|
+
if state == "block":
|
|
105
|
+
if ch == "*" and nxt == "/":
|
|
106
|
+
state = "code"; i += 2; continue
|
|
107
|
+
if ch == "\n":
|
|
108
|
+
out.append("\n")
|
|
109
|
+
i += 1; continue
|
|
110
|
+
# inside a string literal
|
|
111
|
+
if ch == "\\":
|
|
112
|
+
i += 2; continue
|
|
113
|
+
if ch == state:
|
|
114
|
+
state = "code"; i += 1; continue
|
|
115
|
+
if ch == "\n":
|
|
116
|
+
out.append("\n")
|
|
117
|
+
if state != "`":
|
|
118
|
+
state = "code"
|
|
119
|
+
i += 1
|
|
120
|
+
return "".join(out)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _function_header(line: str) -> str | None:
|
|
124
|
+
m = RE_FUNC_KW.search(line) or RE_NAMED_KW.search(line) or RE_ARROW.match(line)
|
|
125
|
+
if m:
|
|
126
|
+
return m.group(1)
|
|
127
|
+
m = RE_METHOD.match(line)
|
|
128
|
+
if m and m.group(1) not in CONTROL_WORDS:
|
|
129
|
+
return m.group(1)
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _count_decisions(segment: str, words: tuple[str, ...], ops: tuple[str, ...],
|
|
134
|
+
initial_depth: int = 0) -> tuple[int, int]:
|
|
135
|
+
cyclomatic = 1
|
|
136
|
+
cognitive = 0
|
|
137
|
+
depth = initial_depth
|
|
138
|
+
# `??` counts as ONE decision, `?.` as none — keep in lockstep with
|
|
139
|
+
# src/quality/complexity.ts (countDecisions).
|
|
140
|
+
segment = segment.replace("??", " ? ").replace("?.", ".")
|
|
141
|
+
for m in TOKEN_RE.finditer(segment):
|
|
142
|
+
tok = m.group(0)
|
|
143
|
+
if tok == "{":
|
|
144
|
+
depth += 1
|
|
145
|
+
continue
|
|
146
|
+
if tok == "}":
|
|
147
|
+
depth = max(0, depth - 1)
|
|
148
|
+
continue
|
|
149
|
+
if tok in words or tok in ops:
|
|
150
|
+
cyclomatic += 1
|
|
151
|
+
cognitive += 1 if tok in _BOOL_OPS else 1 + max(0, depth)
|
|
152
|
+
return cyclomatic, cognitive
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _analyze(content: str, lang: str) -> tuple[list[dict], int]:
|
|
156
|
+
"""Returns (functions, loc). functions: [{name,line,cyclomatic,cognitive}]."""
|
|
157
|
+
style = "hash" if lang in ("python", "ruby") else "c"
|
|
158
|
+
indent_based = lang == "python"
|
|
159
|
+
words = PY_DECISION_WORDS if lang == "python" else C_DECISION_WORDS
|
|
160
|
+
ops = PY_DECISION_OPS if lang == "python" else C_DECISION_OPS
|
|
161
|
+
|
|
162
|
+
stripped = _strip_noise(content, style)
|
|
163
|
+
lines = stripped.split("\n")
|
|
164
|
+
loc = sum(1 for ln in lines if ln.strip())
|
|
165
|
+
functions: list[dict] = []
|
|
166
|
+
|
|
167
|
+
if indent_based:
|
|
168
|
+
open_fn: dict | None = None
|
|
169
|
+
buf: list[str] = []
|
|
170
|
+
mod_buf: list[str] = []
|
|
171
|
+
|
|
172
|
+
def flush() -> None:
|
|
173
|
+
nonlocal open_fn, buf
|
|
174
|
+
if open_fn is None:
|
|
175
|
+
return
|
|
176
|
+
seg = "\n".join(buf)
|
|
177
|
+
cc, cog = _count_decisions(seg, words, ops)
|
|
178
|
+
functions.append({"name": open_fn["name"], "line": open_fn["line"],
|
|
179
|
+
"cyclomatic": cc, "cognitive": cog})
|
|
180
|
+
open_fn = None
|
|
181
|
+
buf = []
|
|
182
|
+
|
|
183
|
+
for idx, line in enumerate(lines):
|
|
184
|
+
dm = RE_PY_DEF.match(line)
|
|
185
|
+
if dm:
|
|
186
|
+
flush()
|
|
187
|
+
open_fn = {"name": dm.group(2), "line": idx + 1, "indent": len(dm.group(1))}
|
|
188
|
+
buf.append(line)
|
|
189
|
+
continue
|
|
190
|
+
if open_fn is not None:
|
|
191
|
+
indent = len(line) - len(line.lstrip())
|
|
192
|
+
if line.strip() and indent <= open_fn["indent"]:
|
|
193
|
+
flush()
|
|
194
|
+
mod_buf.append(line) # the dedent line is module-level
|
|
195
|
+
else:
|
|
196
|
+
buf.append(line)
|
|
197
|
+
else:
|
|
198
|
+
mod_buf.append(line)
|
|
199
|
+
flush()
|
|
200
|
+
# Module-level decisions score as a pseudo-function, mirroring
|
|
201
|
+
# src/quality/complexity.ts — signature parity is load-bearing.
|
|
202
|
+
mcc, mcog = _count_decisions("\n".join(mod_buf), words, ops)
|
|
203
|
+
if mcc > 1:
|
|
204
|
+
functions.append({"name": "<module>", "line": 1,
|
|
205
|
+
"cyclomatic": mcc, "cognitive": mcog})
|
|
206
|
+
return functions, loc
|
|
207
|
+
|
|
208
|
+
depth = 0
|
|
209
|
+
open_fn = None
|
|
210
|
+
buf = []
|
|
211
|
+
mod_buf = []
|
|
212
|
+
for idx, line in enumerate(lines):
|
|
213
|
+
if open_fn is None:
|
|
214
|
+
name = _function_header(line)
|
|
215
|
+
if name:
|
|
216
|
+
delta = line.count("{") - line.count("}")
|
|
217
|
+
if delta <= 0:
|
|
218
|
+
if "{" not in line:
|
|
219
|
+
# Multi-line signature — the opening brace is on a
|
|
220
|
+
# later line. Open the region now, anticipating the
|
|
221
|
+
# body one level above current depth.
|
|
222
|
+
open_fn = {"name": name, "line": idx + 1,
|
|
223
|
+
"start_depth": depth + 1, "entered": False}
|
|
224
|
+
buf = [line]
|
|
225
|
+
continue
|
|
226
|
+
# One-line function: the brace balance never dips below
|
|
227
|
+
# start_depth, so without this branch the region stays
|
|
228
|
+
# open and swallows the REST OF THE FILE into this
|
|
229
|
+
# function's metrics. Score the header line alone.
|
|
230
|
+
cc, cog = _count_decisions(line, words, ops, 0)
|
|
231
|
+
functions.append({"name": name, "line": idx + 1,
|
|
232
|
+
"cyclomatic": cc, "cognitive": cog})
|
|
233
|
+
depth = max(0, depth + delta)
|
|
234
|
+
continue
|
|
235
|
+
open_fn = {
|
|
236
|
+
"name": name,
|
|
237
|
+
"line": idx + 1,
|
|
238
|
+
"start_depth": depth + delta,
|
|
239
|
+
"entered": True,
|
|
240
|
+
}
|
|
241
|
+
buf = [line]
|
|
242
|
+
depth += delta
|
|
243
|
+
continue
|
|
244
|
+
mod_buf.append(line)
|
|
245
|
+
depth += line.count("{") - line.count("}")
|
|
246
|
+
if depth < 0:
|
|
247
|
+
depth = 0
|
|
248
|
+
continue
|
|
249
|
+
buf.append(line)
|
|
250
|
+
d = line.count("{") - line.count("}")
|
|
251
|
+
depth += d
|
|
252
|
+
if not open_fn["entered"]:
|
|
253
|
+
if d > 0:
|
|
254
|
+
open_fn["entered"] = True
|
|
255
|
+
continue # never close-check before the body opens
|
|
256
|
+
if depth < open_fn["start_depth"]:
|
|
257
|
+
seg = "\n".join(buf)
|
|
258
|
+
cc, cog = _count_decisions(seg, words, ops, -1)
|
|
259
|
+
functions.append({"name": open_fn["name"], "line": open_fn["line"],
|
|
260
|
+
"cyclomatic": cc, "cognitive": cog})
|
|
261
|
+
open_fn = None
|
|
262
|
+
buf = []
|
|
263
|
+
if open_fn is not None:
|
|
264
|
+
seg = "\n".join(buf)
|
|
265
|
+
cc, cog = _count_decisions(seg, words, ops, -1)
|
|
266
|
+
functions.append({"name": open_fn["name"], "line": open_fn["line"],
|
|
267
|
+
"cyclomatic": cc, "cognitive": cog})
|
|
268
|
+
mcc, mcog = _count_decisions("\n".join(mod_buf), words, ops)
|
|
269
|
+
if mcc > 1:
|
|
270
|
+
functions.append({"name": "<module>", "line": 1,
|
|
271
|
+
"cyclomatic": mcc, "cognitive": mcog})
|
|
272
|
+
return functions, loc
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _count_any(content: str, lang: str) -> int:
|
|
276
|
+
pat = ANY_PATTERNS.get(lang)
|
|
277
|
+
if not pat:
|
|
278
|
+
return 0
|
|
279
|
+
style = "hash" if lang in ("python", "ruby") else "c"
|
|
280
|
+
stripped = _strip_noise(content, style)
|
|
281
|
+
return sum(len(pat.findall(line)) for line in stripped.split("\n"))
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _violations(rel_path: str, content: str, thresholds: dict) -> list[dict]:
|
|
285
|
+
lang = EXT_LANG.get(Path(rel_path).suffix.lower())
|
|
286
|
+
if not lang:
|
|
287
|
+
return []
|
|
288
|
+
out: list[dict] = []
|
|
289
|
+
functions, loc = _analyze(content, lang)
|
|
290
|
+
|
|
291
|
+
max_loc = thresholds.get("maxLocPerFile", 500)
|
|
292
|
+
if loc > max_loc:
|
|
293
|
+
out.append({"signature": f"{rel_path}::locPerFile::<file>", "value": loc,
|
|
294
|
+
"message": f"{rel_path}: {loc} LOC exceeds max {max_loc}"})
|
|
295
|
+
|
|
296
|
+
max_cc = thresholds.get("maxCyclomatic", 22)
|
|
297
|
+
max_cog = thresholds.get("maxCognitive", 22)
|
|
298
|
+
for fn in functions:
|
|
299
|
+
if fn["cyclomatic"] > max_cc:
|
|
300
|
+
out.append({"signature": f"{rel_path}::cyclomatic::{fn['name']}@{fn['line']}",
|
|
301
|
+
"value": fn["cyclomatic"],
|
|
302
|
+
"message": f"{rel_path}:{fn['line']} {fn['name']}() cyclomatic {fn['cyclomatic']} exceeds max {max_cc}"})
|
|
303
|
+
if fn["cognitive"] > max_cog:
|
|
304
|
+
out.append({"signature": f"{rel_path}::cognitive::{fn['name']}@{fn['line']}",
|
|
305
|
+
"value": fn["cognitive"],
|
|
306
|
+
"message": f"{rel_path}:{fn['line']} {fn['name']}() cognitive {fn['cognitive']} exceeds max {max_cog}"})
|
|
307
|
+
|
|
308
|
+
max_any = thresholds.get("maxAnyTypes", 0)
|
|
309
|
+
n_any = _count_any(content, lang)
|
|
310
|
+
if n_any > max_any:
|
|
311
|
+
out.append({"signature": f"{rel_path}::anyTypes::<file>", "value": n_any,
|
|
312
|
+
"message": f"{rel_path}: {n_any} explicit any/unknown usage(s) exceed max {max_any}"})
|
|
313
|
+
return out
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _load_json(root: Path, name: str) -> dict | None:
|
|
317
|
+
for base in (worktree_root(), root):
|
|
318
|
+
p = base / ".uap" / name
|
|
319
|
+
if p.exists():
|
|
320
|
+
try:
|
|
321
|
+
return json.loads(p.read_text())
|
|
322
|
+
except Exception: # noqa: BLE001
|
|
323
|
+
return None
|
|
324
|
+
return None
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _target_path(args: dict) -> str:
|
|
328
|
+
for key in ("file_path", "path", "notebook_path", "filePath", "target"):
|
|
329
|
+
if args.get(key):
|
|
330
|
+
return str(args[key])
|
|
331
|
+
return ""
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _post_edit_content(op: str, args: dict, target: str) -> str:
|
|
335
|
+
"""Best reconstruction of the file AFTER the edit.
|
|
336
|
+
|
|
337
|
+
Write: the proposed content. Edit: old_string replaced by new_string in
|
|
338
|
+
the on-disk file (first occurrence). MultiEdit: all edits applied in
|
|
339
|
+
order. Falls back to the inserted text when reconstruction is impossible —
|
|
340
|
+
partial scans still catch over-threshold inserts.
|
|
341
|
+
"""
|
|
342
|
+
if args.get("content") is not None:
|
|
343
|
+
return str(args["content"])
|
|
344
|
+
disk = ""
|
|
345
|
+
if target:
|
|
346
|
+
try:
|
|
347
|
+
disk = Path(target).read_text()
|
|
348
|
+
except OSError:
|
|
349
|
+
disk = ""
|
|
350
|
+
edits = args.get("edits")
|
|
351
|
+
if isinstance(edits, list):
|
|
352
|
+
out = disk
|
|
353
|
+
for e in edits:
|
|
354
|
+
if not isinstance(e, dict):
|
|
355
|
+
continue
|
|
356
|
+
old, new = str(e.get("old_string", "")), str(e.get("new_string", ""))
|
|
357
|
+
out = out.replace(old, new, 1) if old and old in out else out + "\n" + new
|
|
358
|
+
return out
|
|
359
|
+
if args.get("new_string") is not None:
|
|
360
|
+
new = str(args["new_string"])
|
|
361
|
+
old = str(args.get("old_string", ""))
|
|
362
|
+
if disk and old and old in disk:
|
|
363
|
+
return disk.replace(old, new, 1)
|
|
364
|
+
return disk + "\n" + new if disk else new
|
|
365
|
+
return ""
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _block_message(blocking: list[dict]) -> str:
|
|
369
|
+
lines = ["quality-metrics gate: this edit introduces NEW or WORSENED metric debt:"]
|
|
370
|
+
for v in blocking[:6]:
|
|
371
|
+
lines.append(f" ✗ {v['message']}")
|
|
372
|
+
if len(blocking) > 6:
|
|
373
|
+
lines.append(f" … and {len(blocking) - 6} more")
|
|
374
|
+
lines.append(" Fix the violation, keep it at/below the baselined value, or regenerate the")
|
|
375
|
+
lines.append(" baseline deliberately: uap quality baseline --update (reviewable in git).")
|
|
376
|
+
lines.append(" Full metrics (coverage/CRAP/duplication/mutation): uap quality check.")
|
|
377
|
+
lines.append(" Escape hatch: UAP_QUALITY_GATE_OFF=1")
|
|
378
|
+
return "\n".join(lines)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _rel_path(target: str, root: Path) -> str:
|
|
382
|
+
try:
|
|
383
|
+
rel = str(Path(target).resolve().relative_to(root.resolve()))
|
|
384
|
+
except (ValueError, OSError):
|
|
385
|
+
rel = target
|
|
386
|
+
return rel.replace("\\", "/")
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _in_scope(rel: str, config: dict) -> str | None:
|
|
390
|
+
"""Honour the config's own scope, or the enforcer is STRICTER than the CLI
|
|
391
|
+
(e.g. blocking a Write into dist/ output that `uap quality check`
|
|
392
|
+
ignores). Mirrors isScannableSource in src/quality/scanner.ts. Returns a
|
|
393
|
+
skip reason, or None when the file is in scope."""
|
|
394
|
+
source_exts = config.get("sourceExts")
|
|
395
|
+
if isinstance(source_exts, list) and Path(rel).suffix.lower() not in source_exts:
|
|
396
|
+
return "extension not in quality-metrics.json sourceExts"
|
|
397
|
+
for excl in config.get("excludeDirs", []):
|
|
398
|
+
if f"/{excl}/" in f"/{rel}" or rel.startswith(f"{excl}/"):
|
|
399
|
+
return f"excluded dir ({excl})"
|
|
400
|
+
return None
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def main() -> None:
|
|
404
|
+
op, args = parse_cli()
|
|
405
|
+
|
|
406
|
+
if os.environ.get("UAP_QUALITY_GATE_OFF") == "1":
|
|
407
|
+
emit(True, "UAP_QUALITY_GATE_OFF override set")
|
|
408
|
+
if op not in PATH_WRITE_OPS:
|
|
409
|
+
emit(True, "not a file-write operation")
|
|
410
|
+
|
|
411
|
+
target = _target_path(args)
|
|
412
|
+
lang = EXT_LANG.get(Path(target).suffix.lower())
|
|
413
|
+
if not lang:
|
|
414
|
+
emit(True, "not a scannable source file")
|
|
415
|
+
|
|
416
|
+
config = _load_json(repo_root(), "quality-metrics.json")
|
|
417
|
+
if not config:
|
|
418
|
+
emit(True, "no .uap/quality-metrics.json — quality gate inactive")
|
|
419
|
+
thresholds = config.get("thresholds", {})
|
|
420
|
+
|
|
421
|
+
content = _post_edit_content(op, args, target)
|
|
422
|
+
if not content:
|
|
423
|
+
emit(True, "no post-edit content to scan")
|
|
424
|
+
|
|
425
|
+
rel = _rel_path(target, worktree_root())
|
|
426
|
+
skip = _in_scope(rel, config)
|
|
427
|
+
if skip:
|
|
428
|
+
emit(True, skip)
|
|
429
|
+
|
|
430
|
+
violations = _violations(rel, content, thresholds)
|
|
431
|
+
if not violations:
|
|
432
|
+
emit(True, "within quality thresholds")
|
|
433
|
+
|
|
434
|
+
baseline = _load_json(repo_root(), "quality-baseline.json") or {}
|
|
435
|
+
entries = {e.get("signature"): e.get("value", 0)
|
|
436
|
+
for e in baseline.get("entries", []) if isinstance(e, dict)}
|
|
437
|
+
|
|
438
|
+
blocking = [v for v in violations
|
|
439
|
+
if v["signature"] not in entries or v["value"] > entries[v["signature"]]]
|
|
440
|
+
if not blocking:
|
|
441
|
+
emit(True, f"{len(violations)} violation(s) grandfathered by the quality baseline")
|
|
442
|
+
|
|
443
|
+
emit(False, _block_message(blocking))
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
if __name__ == "__main__":
|
|
447
|
+
main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -39,7 +39,8 @@ like `feature/foo` and `feature-foo` never collide on one artifact. Recognised
|
|
|
39
39
|
shape:
|
|
40
40
|
|
|
41
41
|
```json
|
|
42
|
-
{ "branch": "<name>", "head": "<sha>", "verdict": "approve", "reviewers": ["code-quality-reviewer", "security-code-reviewer", "..."]
|
|
42
|
+
{ "branch": "<name>", "head": "<sha>", "verdict": "approve", "reviewers": ["code-quality-reviewer", "security-code-reviewer", "..."],
|
|
43
|
+
"quality": { "pass": true, "blocking": 0, "report": ".uap/quality-report.json" } }
|
|
43
44
|
```
|
|
44
45
|
|
|
45
46
|
If the artifact records a `branch` that differs from the current branch, or a
|
|
@@ -48,6 +49,13 @@ stale) and the op is blocked until a fresh review is recorded. Including
|
|
|
48
49
|
`branch` and `head` is strongly recommended so the artifact unambiguously
|
|
49
50
|
identifies what it covers.
|
|
50
51
|
|
|
52
|
+
The optional `quality` block fuses the **quality-metrics gate** into the
|
|
53
|
+
review: when the artifact carries `"quality": {"pass": false, ...}` (written
|
|
54
|
+
from `uap quality check --json`), the ship is blocked until the machine
|
|
55
|
+
metrics pass — reviewers adjudicate the numbers, they cannot outvote them.
|
|
56
|
+
An absent or `null` quality block fails open (gate inactive / older
|
|
57
|
+
artifacts).
|
|
58
|
+
|
|
51
59
|
## Why
|
|
52
60
|
|
|
53
61
|
The `parallel-expert-review` skill (and the `architect-reviewer` droid) claim
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# quality-metrics-gate
|
|
2
|
+
|
|
3
|
+
**Category**: quality
|
|
4
|
+
**Level**: REQUIRED
|
|
5
|
+
**Enforcement Stage**: pre-exec
|
|
6
|
+
**Tags**: quality, complexity, coverage, mutation, duplication, metrics
|
|
7
|
+
|
|
8
|
+
## Rule
|
|
9
|
+
|
|
10
|
+
A `Write`, `Edit`, or `MultiEdit` to a source file whose post-edit content
|
|
11
|
+
violates the thresholds in `.uap/quality-metrics.json` is **blocked** unless
|
|
12
|
+
the violation is already recorded in `.uap/quality-baseline.json` at an
|
|
13
|
+
equal-or-worse value. This is a **ratchet**: existing debt is frozen in the
|
|
14
|
+
baseline, new or worsened debt is refused.
|
|
15
|
+
|
|
16
|
+
Default thresholds (configurable per project):
|
|
17
|
+
|
|
18
|
+
- Cyclomatic complexity < 22 per function
|
|
19
|
+
- Cognitive complexity < 22 per function
|
|
20
|
+
- Halstead difficulty < 80 per function (via rust-code-analysis, when installed)
|
|
21
|
+
- Lines of code < 500 per file
|
|
22
|
+
- Test coverage 100% (via coverage-summary.json, at commit/CI time)
|
|
23
|
+
- CRAP score < 25 per function (cc²·(1−cov)³ + cc)
|
|
24
|
+
- Surviving mutants: 0 (Stryker incremental on changed files, via `uap quality mutate`)
|
|
25
|
+
- Dead code: 0 (knip / vulture, when installed)
|
|
26
|
+
- Redundant code: 0 (jscpd, when installed)
|
|
27
|
+
- Explicit `any`/`unknown` types: 0
|
|
28
|
+
|
|
29
|
+
The gate is **inactive** (fails open) when `.uap/quality-metrics.json` is
|
|
30
|
+
absent — policing is opt-in per project via `uap quality init`. Languages
|
|
31
|
+
without installed tooling are skipped with a logged warning; the built-in
|
|
32
|
+
heuristic scanner (LOC, cyclomatic, cognitive, any-types) always runs for
|
|
33
|
+
TS/JS/Py/Java/C#/C/C++/Rust/Go and similar brace or indent languages.
|
|
34
|
+
|
|
35
|
+
## Why
|
|
36
|
+
|
|
37
|
+
Quality metrics only police outcomes when enforcement is deterministic and
|
|
38
|
+
immediate. An LLM reviewer asked "is cognitive complexity < 22?" estimates;
|
|
39
|
+
a gate that computes the number at edit time does not. The baseline ratchet
|
|
40
|
+
makes the strict thresholds adoptable on real codebases: day one, the gate
|
|
41
|
+
enforces "never regress"; the baseline then only shrinks as debt is paid
|
|
42
|
+
down. Judgment calls (waivers, gaming detection — assertion-free tests,
|
|
43
|
+
complexity split into worse trivial functions) belong to the parallel expert
|
|
44
|
+
review, which consumes the machine report via the `quality` block of the
|
|
45
|
+
review artifact.
|
|
46
|
+
|
|
47
|
+
## Enforcement
|
|
48
|
+
|
|
49
|
+
Python enforcer `quality_metrics_gate.py` (stdlib-only) computes the fast-path metrics
|
|
50
|
+
(LOC, cyclomatic, cognitive, any-types) on the reconstructed post-edit content
|
|
51
|
+
and ratchets against `.uap/quality-baseline.json`. It mirrors
|
|
52
|
+
`src/quality/{scanner,complexity,baseline}.ts` — a parity test
|
|
53
|
+
(`test/quality/quality-gate-enforcer.test.ts`) locks the violation signatures
|
|
54
|
+
the two implementations produce, since the CLI-written baseline is consumed by
|
|
55
|
+
the enforcer.
|
|
56
|
+
|
|
57
|
+
Slow metrics (coverage, CRAP, Halstead, duplication, dead code, mutation) are
|
|
58
|
+
enforced by `uap quality check` at commit/CI time, not per edit.
|
|
59
|
+
|
|
60
|
+
Escape hatch: `UAP_QUALITY_GATE_OFF=1`. Deliberate debt acceptance:
|
|
61
|
+
`uap quality baseline --update` (committed, reviewable in git).
|
|
62
|
+
|
|
63
|
+
**Scanner upgrade caveat:** a baseline generated with the built-in heuristic
|
|
64
|
+
scanner uses heuristic signatures (`name@line`). Installing lizard or
|
|
65
|
+
rust-code-analysis later re-identifies complexity violations with
|
|
66
|
+
authoritative names/offsets, so previously grandfathered entries may block
|
|
67
|
+
once after the upgrade. Regenerate the baseline (`uap quality baseline
|
|
68
|
+
--update`) when adopting an external scanner.
|
|
69
|
+
|
|
70
|
+
```rules
|
|
71
|
+
- title: "New or worsened metric debt is blocked; the baseline only shrinks"
|
|
72
|
+
keywords: [complexity, cyclomatic, cognitive, halstead, coverage, CRAP, mutation, duplication, dead code, any type, quality gate, ratchet]
|
|
73
|
+
antiPatterns: ["function over complexity threshold", "file over 500 LOC", "explicit any", "untested new code", "baseline regeneration without review"]
|
|
74
|
+
```
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<!-- Optimizations #22-27: Template Compression, Structured Iteration, Inline Domain Knowledge, Early Impossibility Exit, Conditional Context, Remove Emphasis Theater -->
|
|
3
3
|
<!-- Optimizations #28-34: Mandatory Verifier Loop, Decoder-First Gate, Context Stripping, Environment Check, Schema Diff, State Protection, Conditional Domain -->
|
|
4
4
|
|
|
5
|
-
<!-- ENFORCEMENT_CHECKS: SESSION_START,DECISION_LOOP,MANDATORY_WORKTREE,PARALLEL_REVIEW,SCHEMA_DIFF,GATES,
|
|
5
|
+
<!-- ENFORCEMENT_CHECKS: SESSION_START,DECISION_LOOP,MANDATORY_WORKTREE,PARALLEL_REVIEW,SCHEMA_DIFF,GATES,HOOK_INCLUDES,PATTERN_ROUTER,VALIDATE_PLAN -->
|
|
6
6
|
<!-- TEMPLATE_VERSION: 2.3.0 -->
|
|
7
7
|
<!-- LAST_VALIDATED: 2026-03-09 -->
|
|
8
8
|
|
|
Binary file
|
|
@@ -22,6 +22,49 @@ if [ -z "$CMD" ]; then
|
|
|
22
22
|
exit 0
|
|
23
23
|
fi
|
|
24
24
|
|
|
25
|
+
# ─── Worktree Enforcement Opt-Out ───────────────────────────────
|
|
26
|
+
# The worktree guards below (direct master commit/push, destructive git
|
|
27
|
+
# outside worktrees) only apply when the project mandates worktrees. Setup
|
|
28
|
+
# wizard "Worktree isolation" off → .uap.json worktrees.enforce=false and the
|
|
29
|
+
# guards stand down. Unreadable/missing config = enforce (backward
|
|
30
|
+
# compatible). UAP_NO_WORKTREE=1 is the per-run escape hatch for the
|
|
31
|
+
# commit/push guards (the destructive-git guard keys on the config only — it
|
|
32
|
+
# is data-loss prevention, not a workflow mandate).
|
|
33
|
+
# (The protocol-tag, infra-protect, IaC, force-push, and version-edit guards
|
|
34
|
+
# above/below are independent of the worktree workflow and stay on.)
|
|
35
|
+
#
|
|
36
|
+
# The config lives at the MAIN checkout root and is typically gitignored, so
|
|
37
|
+
# worktree checkouts don't carry it: resolve the main root via git-common-dir
|
|
38
|
+
# (a linked worktree's toplevel would miss the file and silently re-arm).
|
|
39
|
+
# Probe only for git commands — non-git payloads skip the python3 spawn.
|
|
40
|
+
_WT_ENFORCE="on"
|
|
41
|
+
_WT_CFG="on"
|
|
42
|
+
if echo "$CMD" | grep -qE '\bgit\b'; then
|
|
43
|
+
_WT_CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null || true)
|
|
44
|
+
_WT_COMMON="$(git -C "${_WT_CWD:-.}" rev-parse --path-format=absolute --git-common-dir 2>/dev/null || true)"
|
|
45
|
+
_WT_ROOT=""
|
|
46
|
+
if [ -n "$_WT_COMMON" ]; then
|
|
47
|
+
_WT_ROOT="$(dirname "$_WT_COMMON" 2>/dev/null || true)"
|
|
48
|
+
fi
|
|
49
|
+
if [ -z "$_WT_ROOT" ] || [ ! -d "$_WT_ROOT" ]; then
|
|
50
|
+
_WT_ROOT="$(cd "$HOOK_DIR/../.." 2>/dev/null && pwd || true)"
|
|
51
|
+
fi
|
|
52
|
+
if [ -n "$_WT_ROOT" ]; then
|
|
53
|
+
_WT_CFG="$(python3 -c '
|
|
54
|
+
import json, sys
|
|
55
|
+
try:
|
|
56
|
+
cfg = json.load(open(sys.argv[1]))
|
|
57
|
+
print("off" if (cfg.get("worktrees") or {}).get("enforce") is False else "on")
|
|
58
|
+
except Exception:
|
|
59
|
+
print("on")
|
|
60
|
+
' "$_WT_ROOT/.uap.json" 2>/dev/null || echo on)"
|
|
61
|
+
_WT_ENFORCE="$_WT_CFG"
|
|
62
|
+
fi
|
|
63
|
+
fi
|
|
64
|
+
if [ "${UAP_NO_WORKTREE:-0}" = "1" ]; then
|
|
65
|
+
_WT_ENFORCE="off"
|
|
66
|
+
fi
|
|
67
|
+
|
|
25
68
|
# ─── Protocol Tag Injection Guard ────────────────────────────────
|
|
26
69
|
# Reject Bash payloads that still contain standalone protocol tag lines.
|
|
27
70
|
# These fragments can appear after malformed tool-call rendering and must
|
|
@@ -78,7 +121,8 @@ fi
|
|
|
78
121
|
|
|
79
122
|
# ─── Direct Master/Main Commit Protection ───────────────────────
|
|
80
123
|
# Block git commit when on master/main AND not inside a worktree
|
|
81
|
-
|
|
124
|
+
# (skipped when the project opted out of worktree enforcement)
|
|
125
|
+
if [ "$_WT_ENFORCE" != "off" ] && echo "$CMD" | grep -qE '\bgit\s+commit\b'; then
|
|
82
126
|
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
|
|
83
127
|
CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null || true)
|
|
84
128
|
CHECK_DIR="${CWD:-$PROJECT_DIR}"
|
|
@@ -99,7 +143,8 @@ fi
|
|
|
99
143
|
|
|
100
144
|
# ─── Direct Push to Master/Main Protection ──────────────────────
|
|
101
145
|
# Block git push targeting main/master directly (not through PR)
|
|
102
|
-
|
|
146
|
+
# (skipped when the project opted out of worktree enforcement)
|
|
147
|
+
if [ "$_WT_ENFORCE" != "off" ] && echo "$CMD" | grep -qE '\bgit\s+push\b'; then
|
|
103
148
|
# Block explicit pushes to main/master
|
|
104
149
|
if echo "$CMD" | grep -qE '\bgit\s+push\s+(origin\s+)?(main|master)\b'; then
|
|
105
150
|
# Allow push after version bump (git push && git push --tags pattern)
|
|
@@ -113,7 +158,9 @@ fi
|
|
|
113
158
|
|
|
114
159
|
# ─── Destructive Git Operations ─────────────────────────────────
|
|
115
160
|
# Block git reset --hard and git clean -f outside worktrees
|
|
116
|
-
|
|
161
|
+
# (keys on the project config only — data-loss prevention, not a per-run
|
|
162
|
+
# workflow choice, so UAP_NO_WORKTREE does not stand it down)
|
|
163
|
+
if [ "$_WT_CFG" != "off" ] && echo "$CMD" | grep -qE '\bgit\s+reset\s+--hard\b|\bgit\s+clean\s+-[a-z]*f'; then
|
|
117
164
|
CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null || true)
|
|
118
165
|
if ! echo "${CWD:-.}" | grep -q '\.worktrees/'; then
|
|
119
166
|
echo "BLOCKED [git-safety]: Destructive git operations (reset --hard, clean -f) are prohibited outside worktrees. These can destroy uncommitted work in the project root." >&2
|